Form

Forms contain fields of controls to enter information for submission.

Give FeedbackBundle Size
App login

Username and password are both admin to log in.

'use client';

import * as React from 'react';
import { Form } from '@base_ui/react/Form';
import { Fieldset } from '@base_ui/react/Fieldset';
import { Field } from '@base_ui/react/Field';
import { TextInput } from '@base_ui/react/TextInput';
import { styled } from '@mui/system';

type Status = 'initial' | 'loading' | 'success' | 'error';

export default function FormIntroduction() {
  const [errors, setErrors] = React.useState({});
  const [status, setStatus] = React.useState<Status>('initial');

  return (
    <FormRoot
      errors={errors}
      onClearErrors={setErrors}
      onSubmit={async (event) => {
        event.preventDefault();

        const formData = new FormData(event.currentTarget);
        const username = formData.get('username') as string;
        const password = formData.get('password') as string;

        setStatus('loading');

        // Mimic a server request
        await new Promise((resolve) => {
          setTimeout(resolve, 500);
        });

        const isUnknownUser = username !== 'admin';
        const isInvalidPassword = password !== 'admin';

        const serverErrors: Partial<Record<'username' | 'password', string>> = {};

        if (isUnknownUser) {
          serverErrors.username = 'Username does not exist.';
          setStatus('error');
        } else if (isInvalidPassword) {
          serverErrors.password = 'Invalid password.';
          setStatus('error');
        } else {
          setStatus('success');
        }

        setErrors(serverErrors);
      }}
    >
      <FieldsetRoot>
        <FieldsetLegend>App login</FieldsetLegend>
        <p>
          Username and password are both <code>admin</code> to log in.
        </p>
        <Field.Root name="username">
          <Field.Label>Username</Field.Label>
          <Input required />
          <FieldError />
        </Field.Root>
        <Field.Root name="password">
          <Field.Label>Password</Field.Label>
          <Input type="password" required />
          <FieldError />
        </Field.Root>
      </FieldsetRoot>
      <FormSubmit disabled={status === 'loading'}>
        {status === 'loading' ? 'Logging in...' : 'Log in'}
      </FormSubmit>
      {status === 'success' && (
        <FormSuccess role="alert" aria-live="polite">
          Successfully logged in
        </FormSuccess>
      )}
    </FormRoot>
  );
}

const FormRoot = styled(Form.Root)`
  width: 275px;
`;

const Input = styled(TextInput)`
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 100%;
  padding: 6px;
  font-size: 100%;

  &[data-invalid] {
    border-color: red;
    background-color: rgb(255 0 0 / 0.1);
  }

  &:focus {
    outline: 0;
    border-color: #0078d4;
    box-shadow: 0 0 0 3px rgba(0 100 255 / 0.3);

    &[data-invalid] {
      border-color: red;
      box-shadow: 0 0 0 3px rgba(255 0 0 / 0.3);
    }
  }
`;

const FieldError = styled(Field.Error)`
  font-size: 90%;
  margin: 0;
  margin-bottom: 0;
  margin-top: 4px;
  line-height: 1.1;
  color: red;
`;

const FormSuccess = styled('p')`
  font-size: 90%;
  margin: 0;
  padding: 0;
  margin-top: 4px;
  color: green;
`;

const FieldsetRoot = styled(Fieldset.Root)`
  border: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;

  p {
    margin: 0;
    color: grey;
    font-size: 90%;
  }
`;

const FieldsetLegend = styled(Fieldset.Legend)`
  display: block;
  font-size: 110%;
  font-weight: 600;
`;

const FormSubmit = styled('button')`
  display: block;
  margin-top: 10px;
  padding: 10px;
  width: 100%;
  font-size: 100%;
  background-color: #0078d4;
  color: white;
  border: none;
  border-radius: 4px;

  &[aria-disabled='true'] {
    background-color: #ddd;
    color: black;
  }
`;

Installation

Base UI components are all available as a single package.

npm install @base_ui/react

Once you have the package installed, import the component.

import { Form } from '@base_ui/react/Form';
import { Field } from '@base_ui/react/Field';

Anatomy

Forms are implemented using a Root component and Field components:

<Form.Root>
  <Field.Root />
</Form.Root>

Usage

Forms are intended to be used with the Field component, which provides labeling and validation for individual form controls. These are nested inside Form.Root:

import { Form } from '@base_ui/react/Form';
import { Field } from '@base_ui/react/Field';
<Form.Root>
  <Field.Root>
    <Field.Control />
  </Field.Root>
  <button type="submit">Submit</button>
</Form.Root>

If any of the Fields within the Form are invalid upon submit, focus is moved to the first invalid Field's control and the submit event is prevented.

Validation

The Field.Error subcomponent of a Field renders error messages inside of it, with its content automatically populated with any client-side validation messages that occur.

<Field.Root>
  <Field.Control />
  <Field.Error />
</Field.Root>

Server-side validation

For server-side validation messages, the Form.Root component accepts an errors prop — an object whose keys map to the Field name prop, with each value being a string or array of strings representing error messages. The onClearErrors prop is called to clear these external server errors when the field's control has been changed:

const [errors, setErrors] = React.useState({});
 
async function handleSubmit(event) {
  event.preventDefault();
 
  const formData = Object.fromEntries(new FormData(event.currentTarget));
 
  try {
    await submitForm(formData);
  } catch (errors) {
    // Map errors from the server response
    setErrors({
      username: errors.username,
    });
  }
}
 
return (
  <Form.Root onSubmit={handleSubmit} errors={errors} onClearErrors={setErrors}>
    <Field.Root name="username">
      <Field.Control />
      <Field.Error /> {/* Populated with `errors.username` string */}
    </Field.Root>
  </Form.Root>
);

For more flexibility if required, each Field.Root component accepts an invalid boolean prop, and each Field.Error subcomponent accepts a forceShow boolean prop. These can be used as an alternative to Form.Root's errors prop by manually targeting specific Fields and showing specific error messages.

Native validation

By default, browser-native validation popups are disabled, as Field.Error replaces this by rendering the validation messages to allow for flexible styling. If necessary, to enable these native validation popups, re-apply the default prop:

<Form.Root noValidate={false}>

API Reference

FormRoot

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
errorsobjectObject of error messages with each key mapping to the name prop of a Field control, usually from server-side validation.
onClearErrorsfuncCallback fired when the external server-side error messages should be cleared.
renderunionA function to customize rendering of the component.

Contents