Forms & Validation
Build accessible forms with validation.
Forms are built from two pieces: Field, which groups a control with its label,
description, and error message and wires up the accessibility relationships; and
Form, which coordinates the fields and surfaces server-side errors. Both wrap
Base UI primitives.
Anatomy of a field
A Field composes a label, a control, an optional description, and an error
slot. The for/id and aria-describedby associations are handled for you.
Validation
Native constraints
Standard HTML attributes - required, type, min, maxLength, pattern -
are validated automatically. Point FieldError at a specific failure with
match, or render the browser's default message with a bare <FieldError />:
Custom validation
For rules HTML can't express, pass a validate function to the Field. Return a
string (or array of strings) to fail, or null to pass. Control when it runs
with validationMode ('onBlur' or 'onChange'):
validate can be async (return a promise) which is handy for availability
checks against an API.
Shake on invalid
For an extra cue when validation fails, hang the animate-shake utility off the
control's invalid state. Base UI sets data-invalid on the control when the field
fails validation, so put the class on the Input to shake just the control:
The shake plays once, when the field becomes invalid (the moment data-invalid is added) - not on every failed
submit while it stays invalid. Gating it behind motion-safe: keeps it off for users who prefer reduced motion, per
Animation.
Grouping with Fieldset
Use Fieldset and FieldsetLegend to group related fields under a shared label
(an accessible <fieldset>/<legend>):
The Form wrapper
Form ties the fields together and handles submission. Its most useful job is
displaying server-side errors: return them keyed by field name and pass
them to errors, and each Field shows its message in place.
Inputs are uncontrolled by default - read values from FormData on submit, as above. You can still control any field
with value/onValueChange (or onChange) when you need to.
`Button` does not submit unless you say so
Button renders type="button", which it inherits from Base UI's button behavior, so dropping one inside a <form>
won't submit it. Write type="submit" explicitly, as every example on this page does. An explicit type always
wins, because Base UI merges caller props last.
Submitting to a server action
Form renders a real <form>, so it takes an action. Every control that
carries a name ends up in the FormData, including the ones that aren't native
inputs: Select and Checkbox render their own hidden input for exactly this.
You don't repeat the name on the control. Field's name propagates to whatever
it wraps, and it's the same key Form matches server errors against.
Returning { errors } from the action feeds Form, which routes each message to
the Field whose name matches, so server-side validation renders in the same
place as client-side validation.
items on Select is what lets the closed trigger show "Free" rather than the raw free. It's needed because the
SelectItems live in a portal that isn't mounted while the popup is closed - see
Select.
Accessibility
Labels, descriptions, and error messages are associated with their control
automatically, and invalid fields are marked with aria-invalid. See
Accessibility - and note that grouped controls
(RadioGroup, CheckboxGroup) are labeled with aria-label/aria-labelledby,
not FieldLabel.