How to build an accessible Form in React
Renders a native <form> with noValidate so validation messages come from the component (surfaced per field via Input error/role="alert") rather than inconsistent browser bubbles, while Enter-to-submit and the form role are preserved by the platform element.
When to use a Form
- Collecting and submitting a set of related field values together
- Running sync or async validation (including Standard Schema like zod/valibot/arktype) before invoking onValid
- Tracking per-field touched state and errors via the signal-based store from useForm/createForm
When not to use it
- A single standalone value with no submission step — render a bare Input
- Inline editing of one read-only value — use Editable
Keyboard interactions
Role form, verified at WCAG 2.2-AA.
TabEnter
Common mistakes
Avoid: <form onSubmit={...}> with hand-rolled useState per field
Prefer: const form = useForm({ initialValues, validate }); <Form form={form} onValid={...}>
The store centralizes values, errors, touched, and submitting as signals; rolling your own reintroduces the re-render and validation-timing bugs the store solves
Example
function Demo() {
const form = useForm({
initialValues: { email: '' },
validate: (v) => v.email.includes('@') ? {} : { email: 'Invalid email' },
})
const email = form.field('email')
return (
<Form form={form} onValid={console.log}>
<Input
label="Email"
value={email.value}
onChange={(e) => email.onChange(e.currentTarget.value)}
onBlur={email.onBlur}
error={email.error}
/>
<Button type="submit">Save</Button>
</Form>
)
}