Beta Preview

Forms

Forms allow users to enter and submit data, and provide them with feedback along the way. React Aria includes many components that integrate with HTML forms, with support for custom validation and styling.

Labels and help text

Accessible forms start with clear, descriptive labels for each field. Rendering a <Label> within a field automatically associates it with the input. Additional context can also be added via a secondary description slot. The label and description are announced by screen readers when the field is focused.

Password must be at least 8 characters.
import {TextField, Label, Input, Text} from 'react-aria-components';

<TextField type="password">
  <Label>Password</Label>
  <Input className="react-aria-Input inset" placeholder="Choose a password" />
  <Text slot="description" className="field-description">
    Password must be at least 8 characters.
  </Text>
</TextField>

Most fields should have a visible label. In rare exceptions, the aria-label or aria-labelledby attribute must be provided for assistive technologies.

Submitting data

How you submit form data depends on your framework, application, and server. By default, HTML forms are submitted by the browser using a full page refresh. You can take control of form submission using the action prop or onSubmit event.

Uncontrolled forms

When using React 19, use the action prop to handle form submission. This receives a FormData object containing the values for each form field. In React 18 or earlier, use the onSubmit event instead.

import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';

<Form
  action={formData => {
    let name = formData.get('name');
    alert(`Hello, ${name}!`);
  }}>
  <TextField name="name" label="Name" placeholder="Enter your full name" />
  <Button type="submit">Submit</Button>
</Form>

Controlled forms

By default, all React Aria components are uncontrolled, which means that the state is stored internally on your behalf. To synchronize the value with another part of the UI as the user edits, use the value and onChange props with the useState hook.

You entered:
import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';
import {useState} from 'react';

function Example() {
  let [name, setName] = useState('');

  return (
    <Form
      action={() => {
        // Submit data to your backend API...
        alert(name)
      }}>
      <TextField
        label="Name"
        placeholder="Enter your name"
        value={name}
        onChange={setName} />
      <div>You entered: {name}</div>
      <Button type="submit">Submit</Button>
    </Form>
  );
}

Validation

Well-designed form validation assists the user with specific, helpful error messages without confusing them with unnecessary errors for partial input. React Aria supports native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and integration with server-side validation errors.

Constraint validation

All React Aria form components integrate with HTML constraint validation. This allows you to define constraints on each field such as required, minimum and maximum values, text formats such as email addresses, and custom regular expression patterns. These constraints are checked by the browser when the user commits changes to the value (e.g. on blur) or submits the form.

Use the FieldError component to display validation errors with custom styles rather than the browser's default UI.

import {TextField, Label, Input, FieldError} from 'react-aria-components';
import {Form} from './Form';
import {Button} from './Button';

<Form>
  <TextField name="email" type="email" isRequired>
    <Label>Email</Label>
    <Input className="react-aria-Input inset" placeholder="Enter your email" />
    <FieldError />
  </TextField>
  <Button type="submit">Submit</Button>
</Form>

Supported constraints include:

  • isRequired indicates that a field must have a value before the form can be submitted.
  • minValue and maxValue specify the minimum and maximum value in a date picker or number field.
  • minLength and maxLength specify the minimum and length of text input.
  • pattern provides a custom regular expression that a text input must conform to.
  • type="email" and type="url" provide builtin validation for email addresses and URLs.

See each component's documentation for more details on the supported validation props.

Customizing error messages

By default, the FieldError component displays the error message provided by the browser, which is localized in the user's preferred language. You can customize these messages by providing a render prop function to FieldError. This receives a list of error strings along with a ValidityState object describing why the field is invalid.

import {TextField, Label, Input, FieldError} from 'react-aria-components';
import {Form} from './Form';
import {Button} from './Button';

<Form>
  <TextField name="name" isRequired>
    <Label>Name</Label>
    <Input className="react-aria-Input inset" placeholder="Enter your name" />
    <FieldError>
      {({validationDetails}) => (
        validationDetails.valueMissing ? 'Please enter a name.' : ''
      )}
    </FieldError>
  </TextField>
  <Button type="submit">Submit</Button>
</Form>

Custom validation

To implement custom validation rules, pass a function to the validate prop. This receives the current field value, and can return one or more error messages. These are displayed to the user after the value is committed (e.g. on blur) to avoid distracting them on each keystroke.

import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';

<Form>
  <TextField
    label="Username"
    placeholder="Choose a username"
    validate={value => value === 'admin' ? 'Nice try!' : null} />
  <Button type="submit">Submit</Button>
</Form>

Realtime validation

By default, validation errors are displayed after the value is committed (e.g. on blur), or when the form is submitted. This avoids confusing the user with irrelevant errors while they are still entering a value.

In some cases, validating in realtime can be helpful, such as when meeting password requirements. To implement this, make the field value controlled, and set the isInvalid prop and error message appropriately.

Password must be 8 characters or more.
import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';
import {useState} from 'react';

function Example() {
  let [password, setPassword] = useState('');
  let error;
  if (password.length < 8) {
    error = 'Password must be 8 characters or more.';
  } else if ((password.match(/[A-Z]/g) ?? []).length < 2) {
    error = 'Password must include at least 2 upper case letters';
  } else if ((password.match(/[^a-z]/ig) ?? []).length < 2) {
    error = 'Password must include at least 2 symbols.';
  }

  return (
    <TextField
      label="Password"
      placeholder="Choose a password"
      isInvalid={!!error}
      errorMessage={error}
      value={password}
      onChange={setPassword} />
  );
}

By default, invalid fields block forms from being submitted. To avoid this, use validationBehavior="aria", which will only mark the field as required and invalid for assistive technologies, and will not prevent form submission.

Server validation

Client side validation is useful to give the user immediate feedback, but data should always be validated on the backend for security and reliability. Your business logic may also include rules which cannot be validated on the frontend.

To display server validation errors, set the the validationErrors prop on the Form component. This accepts an object that maps each field's name prop to one or more error messages. These are displayed as soon as the validationErrors prop is set, and cleared after the user modifies each field's value.

import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';
import {useActionState} from 'react';

function action(prevState, formData: FormData) {
  return {
    values: Object.fromEntries(formData),
    errors: {
      username: 'Sorry, this username is taken.'
    }
  };
}

function Example() {
  let [{values, errors}, formAction] = useActionState(action, {});

  return (
    <Form
      validationBehavior="native"
      action={formAction}
      validationErrors={errors}>
      <TextField
        label="Username"
        name="username"
        placeholder="Enter your username"
        defaultValue={values?.username}
        isRequired />
      <TextField
        label="Password"
        name="password"
        placeholder="Enter your password"
        defaultValue={values?.password}
        type="password"
        isRequired />
      <Button type="submit">Submit</Button>
    </Form>
  );
}

Schema validation

React Aria is compatible with errors returned from schema validation libraries like Zod, which are often used for server-side form validation. Use the flatten method to get a list of errors for each field and return this as part of your HTTP response.

// In your server...
import {z} from 'zod';

const schema = z.object({
  name: z.string().min(1),
  age: z.coerce.number().positive()
});

function handleRequest(formData: FormData) {
  let result = schema.safeParse(Object.fromEntries(formData));
  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors
    };
  }

  // Do stuff...

  return {
    errors: {}
  };
}

React Server Functions

Server Functions, marked with the "use server" directive, allow client components to call async functions executed on the server in supported frameworks (e.g. Next.js).

// app/actions.ts
'use server';

export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Create the todo...
  } catch (err) {
    return {
      errors: {
        todo: 'Invalid todo.'
      }
    };
  }
}

Server functions can be imported into client components and passed to the Form action prop. Use the useActionState hook to access the return value, which may include validation errors.

// app/add-form.tsx
'use client';

import {useActionState} from 'react';
import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';
import {createTodo} from './actions';

export function AddForm() {
  let [{errors}, formAction] = useActionState(createTodo, {errors: {}});

  return (
    <Form action={formAction} validationErrors={errors}>
      <TextField label="Task" name="todo" />
      <Button type="submit">Add</Button>
    </Form>
  );
}

React Router actions

React Router actions handle form submissions. Use the useSubmit hook to submit data to the server. An action may return data such as validation errors via the actionData route component prop.

// app/routes/signup.tsx
import {useSubmit} from 'react-router';
import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';

export default function SignupForm({actionData}: Route.ComponentProps) {
  let submit = useSubmit();

  return (
    <Form
      method="post"
      onSubmit={e => {
        e.preventDefault();
        submit(e.currentTarget);
      }}
      validationErrors={actionData?.errors}>
      <TextField label="Username" name="username" isRequired />
      <TextField label="Password" name="password" type="password" isRequired />
      <Button type="submit">Submit</Button>
    </Form>
  );
}

export async function action({request}: Route.ActionArgs) {
  try {
    // Validate data and perform action...
  } catch (err) {
    return {
      errors: {
        username: 'Sorry, this username is taken.'
      }
    };
  }
}

Form libraries

In most cases, uncontrolled forms with the builtin validation features are sufficient. However, if you are building a truly complex form, or integrating React Aria components into an existing form, a separate form library such as React Hook Form or Formik may be helpful.

React Hook Form

React Hook Form is a popular form library for React. It is primarily designed to work directly with plain HTML input elements, but supports custom form components like the ones in React Aria as well.

Use the Controller component from React Hook Form to integrate React Aria components. Pass the props for the field render prop through to the React Aria component you're using, and use the fieldState to get validation errors to display.

import {useForm, Controller} from 'react-hook-form';
import {Form} from './Form';
import {TextField} from './TextField';
import {Button} from './Button';

function App() {
  let {handleSubmit, control} = useForm({
    defaultValues: {
      name: '',
    },
  });
  let onSubmit = (data) => {
    // Call your API here...
  };

  return (
    <Form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        control={control}
        name="name"
        rules={{ required: 'Name is required.' }}
        render={({
          field: { name, value, onChange, onBlur, ref },
          fieldState: { invalid, error },
        }) => (
          <TextField
            label="Name"
            name={name}
            value={value}
            onChange={onChange}
            onBlur={onBlur}
            ref={ref}
            isRequired
            // Let React Hook Form handle validation instead of the browser.
            validationBehavior="aria"
            isInvalid={invalid}
            errorMessage={error?.message} />
        )} />
      <Button type="submit">Submit</Button>
    </Form>
  );
}