Skip to main content
Base UI form control components extend the native constraint validation API so you can build forms for collecting user input or providing control over an interface. They also integrate seamlessly with third-party libraries like React Hook Form and TanStack Form.

Naming form controls

Form controls must have an accessible name in order to be recognized by assistive technologies. <Field.Label> and <Field.Description> automatically assign the accessible name and description to their associated control:
Labeling select and slider
import { Form } from '@base-ui/react/form';
import { Field } from '@base-ui/react/field';
import { Select } from '@base-ui/react/select';
import { Slider } from '@base-ui/react/slider';

<Form>
  <Field.Root>
    <Field.Label>Time zone</Field.Label>
    <Field.Description>Used for notifications and reminders</Field.Description>
    <Select.Root />
  </Field.Root>

  <Field.Root>
    <Field.Label>Zoom level</Field.Label>
    <Field.Description>Adjust the size of the user interface</Field.Description>
    <Slider.Root />
  </Field.Root>
</Form>;
You can implicitly label <Checkbox>, <Radio> and <Switch> components by enclosing them with <Field.Label>:
Implicitly labeling a switch
import { Field } from '@base-ui/react/field';
import { Switch } from '@base-ui/react/switch';

<Field.Root>
  <Field.Label>
    <Switch.Root />
    Developer mode
  </Field.Label>
  <Field.Description>Enables extra tools for web developers</Field.Description>
</Field.Root>;
Compose <Fieldset> when a single label applies to multiple controls, such as a range slider with multiple thumbs or a section that combines several inputs. For checkbox and radio groups, keep the group label in <Fieldset.Legend> and wrap each option with <Field.Item>:
Composing range slider and radio group with fieldset
import { Form } from '@base-ui/react/form';
import { Field } from '@base-ui/react/field';
import { Fieldset } from '@base-ui/react/fieldset';
import { Radio } from '@base-ui/react/radio';
import { RadioGroup } from '@base-ui/react/radio-group';
import { Slider } from '@base-ui/react/slider';

<Form>
  <Field.Root>
    <Fieldset.Root render={<Slider.Root />}>
      <Fieldset.Legend>Price range</Fieldset.Legend>
      <Slider.Control>
        <Slider.Track>
          <Slider.Thumb />
          <Slider.Thumb />
        </Slider.Track>
      </Slider.Control>
    </Fieldset.Root>
  </Field.Root>

  <Field.Root>
    <Fieldset.Root render={<RadioGroup />}>
      <Fieldset.Legend>Storage type</Fieldset.Legend>
      <Radio.Root value="ssd" />
      <Radio.Root value="hdd" />
    </Fieldset.Root>
  </Field.Root>
</Form>;

Building form fields

Pass the name prop to <Field.Root> to include the wrapped control’s value when a parent form is submitted:
Assigning field name to combobox
import { Form } from '@base-ui/react/form';
import { Field } from '@base-ui/react/field';
import { Combobox } from '@base-ui/react/combobox';

<Form>
  <Field.Root name="country">
    <Field.Label>Country of residence</Field.Label>
    <Combobox.Root />
  </Field.Root>
</Form>;

Submitting data

You can take over form submission using the native onSubmit, or custom onFormSubmit props:
Native submission using onSubmit
import { Form } from '@base-ui/react/form';

<Form
  onSubmit={async (event) => {
    // Prevent the browser's default full-page refresh
    event.preventDefault();
    // Create a FormData object
    const formData = new FormData(event.currentTarget);
    // Send the FormData instance in a fetch request
    await fetch('https://api.example.com', {
      method: 'POST',
      body: formData,
    });
  }}
/>;
When using onFormSubmit, you receive form values as a JavaScript object, with eventDetails provided as a second argument. Additionally, preventDefault() is automatically called on the native submit event:
Submission using onFormSubmit
import { Form } from '@base-ui/react/form';

<Form
  onFormSubmit={async (formValues) => {
    const payload = {
      product_id: formValues.id,
      order_quantity: formValues.quantity,
    };
    await fetch('https://api.example.com', {
      method: 'POST',
      body: JSON.stringify(payload),
    });
  }}
/>;

Constraint validation

Base UI form components support native HTML validation attributes for many validation rules:
  • required specifies a required field.
  • minLength and maxLength specify a valid length for text fields.
  • pattern specifies a regular expression that the field value must match.
  • step specifies an increment that numeric field values must be an integral multiple of.
Defining constraint validation on a text field
import { Field } from '@base-ui/react/field';

<Field.Root name="website">
  <Field.Control type="url" required pattern="https?://.*" />
  <Field.Error />
</Field.Root>;
Base UI form components use a hidden input to participate in native form submission and validation. To anchor the hidden input near a control so the native validation bubble points to the correct area, ensure the component has been given a name, and wrap controls in a relatively positioned container for best results.

Custom validation

You can add custom validation logic by passing a synchronous or asynchronous validation function to the validate prop, which runs after native validations have passed. Use the validationMode prop to configure when validation is performed:
  • onSubmit (default) validates all fields when the containing <Form> is submitted, afterwards invalid fields revalidate when their value changes.
  • onBlur validates the field when focus moves away.
  • onChange validates the field when the value changes, for example, after each keypress in a text field or when a checkbox is checked or unchecked.
validationDebounceTime can be used to debounce the function in use cases such as asynchronous requests or text fields that validate onChange.
Text input using custom asynchronous validation
import { Field } from '@base-ui/react/field';

<Field.Root
  name="username"
  validationMode="onChange"
  validationDebounceTime={300}
  validate={async (value) => {
    if (value === 'admin') {
      /* return an error message when invalid */
      return 'Reserved for system use.';
    }

    const result = await fetch(
      /* check the availability of a username from an external API */
    );

    if (!result) {
      return `${value} is unavailable.`;
    }

    /* return `null` when valid */
    return null;
  }}
>
  <Field.Control required minLength={3} />
  <Field.Error />
</Field.Root>;

Server-side validation

You can pass errors returned by (post-submission) server-side validation to the errors prop, which will be merged into the client-side field state for display. This should be an object with field names as keys, and an error string or array of strings as the value. Once a field’s value changes, any corresponding error in errors will be cleared from the field state.
Displaying errors returned by server-side validation
import { Form } from '@base-ui/react/form';
import { Field } from '@base-ui/react/field';

async function submitToServer(/* payload */) {
  return {
    errors: {
      promoCode: 'This promo code has expired',
    },
  };
}

const [errors, setErrors] = React.useState();

<Form
  errors={errors}
  onSubmit={async (event) => {
    event.preventDefault();
    const response = await submitToServer(/* data */);
    setErrors(response.errors);
  }}
>
  <Field.Root name="promoCode" />
</Form>;

Displaying errors

Use <Field.Error> without children to automatically display the field’s native error message when invalid. The match prop can be used to customize the message based on the validity state, and manage internationalization from your application logic:
Customizing error message for a required field
<Field.Error match="valueMissing">You must create a username</Field.Error>

React Hook Form

React Hook Form is a popular library that you can integrate with Base UI to externally manage form and field state for your existing components.

Initialize the form

Initialize the form with the useForm hook, assigning the initial value of each field by their name in the defaultValues parameter:
Initialize a form instance
import { useForm } from 'react-hook-form';

const { control, handleSubmit } = useForm<FormValues>({
  defaultValues: {
    username: '',
    email: '',
  },
});

Integrate components

Use the <Controller> component to integrate with any <Field> component, forwarding the name, field, and fieldState render props to the appropriate part:
Integrating the controller component with Base UI field
import { useForm, Controller } from "react-hook-form"
import { Field } from '@base-ui/react/field';

const { control, handleSubmit} = useForm({
  defaultValues: {
    username: '',
  }
})

<Controller
  name="username"
  control={control}
  render={({
    field: { name, ref, value, onBlur, onChange },
    fieldState: { invalid, isTouched, isDirty, error },
  }) => (
    <Field.Root name={name} invalid={invalid} touched={isTouched} dirty={isDirty}>
      <Field.Label>Username</Field.Label>
      <Field.Description>
        May appear where you contribute or are mentioned. You can remove it at any time.
      </Field.Description>
      <Field.Control
        placeholder="e.g. alice132"
        value={value}
        onBlur={onBlur}
        onValueChange={onChange}
        ref={ref}
      />
      <Field.Error match={!!error}>
        {error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

Field validation

Specify rules on the <Controller> in the same format as register options, and use the match prop to delegate control of the error rendering:
Defining validation rules and displaying errors
import { Controller } from "react-hook-form"
import { Field } from '@base-ui/react/field';

<Controller
  name="username"
  control={control}
  rules={{
    required: 'This is a required field',
    minLength: { value: 2, message: 'Too short' },
    validate: (value) => {
      if (/* custom logic */) {
        return 'Invalid'
      }
      return null;
    },
  }}
  render={({
    field: { name, ref, value, onBlur, onChange },
    fieldState: { invalid, isTouched, isDirty, error },
  }) => (
    <Field.Root name={name} invalid={invalid} touched={isTouched} dirty={isDirty}>
      <Field.Label>Username</Field.Label>
      <Field.Control
        placeholder="e.g. alice132"
        value={value}
        onBlur={onBlur}
        onValueChange={onChange}
        ref={ref}
      />
      <Field.Error match={!!error}>
        {error?.message}
      </Field.Error>
    </Field.Root>
  )}
/>

Submitting data

Wrap your submit handler function with handleSubmit to receive the form values as a JavaScript object for further handling:
Form submission handler
import { useForm } from 'react-hook-form';
import { Form } from '@base-ui/react/form';

interface FormValues {
  username: string;
  email: string;
}

const { handleSubmit } = useForm<FormValues>();

async function submitForm(data: FormValues) {
  // transform the object and/or submit it to a server
  await fetch(/* ... */);
}

<Form onSubmit={handleSubmit(submitForm)} />;

TanStack Form

TanStack Form is a form library with a function-based API for orchestrating validations that can also be integrated with Base UI.

Initialize the form

Create a form instance with the useForm hook, assigning the initial value of each field by their name in the defaultValues parameter:
Initialize a form instance
import { useForm } from '@tanstack/react-form';

interface FormValues {
  username: string;
  email: string;
}

const defaultValues: FormValues = {
  username: '',
  email: '',
};

const form = useForm<FormValues>({
  defaultValues,
});

Integrate components

Use the <form.Field> component from the form instance to integrate with Base UI components using the children prop, forwarding the various field render props to the appropriate part:
Integrating TanStack Form with Base UI components
import { useForm } from '@tanstack/react-form';
import { Field } from '@base-ui/react/field';

const form = useForm(/* defaultValues, other parameters */)

<form>
  <form.Field
    name="username"
    children={(field) => (
      <Field.Root
        name={field.name}
        invalid={!field.state.meta.isValid}
        dirty={field.state.meta.isDirty}
        touched={field.state.meta.isTouched}
      >
        <Field.Label>Username</Field.Label>
        <Field.Control
          value={field.state.value}
          onValueChange={field.handleChange}
          onBlur={field.handleBlur}
          placeholder="e.g. bob276"
        />

        <Field.Error match={!field.state.meta.isValid}>
          {field.state.meta.errors.join(',')}
        </Field.Error>
      </Field.Root>
    )}
  />
</form>

Submitting data

To submit the form:
  1. Pass a submit handler function to the onSubmit parameter of useForm.
  2. Call form.handleSubmit() from an event handler such as form onSubmit or onClick on a button.
Form submission handler
import { useForm } from '@tanstack/react-form';

const form = useForm({
  onSubmit: async ({ value: formValues }) => {
    await fetch(/* POST the formValues to an external API */);
  },
});

<form
  onSubmit={(event) => {
    event.preventDefault();
    form.handleSubmit();
  }}
>
  {/* form fields */}
  <button type="submit">Submit</button>
</form>;