Form
Headless form primitives that handle the boring parts: matching label’s htmlFor to the control’s id, wiring aria-describedby to descriptions and error messages, toggling aria-invalid / aria-required / disabled, and exposing data-invalid / data-required / data-disabled for styling.
You own the inputs — Form.Control clones a single child and injects the right attributes. Validation logic is yours; the Form simply renders Form.Error when the parent Form.Field is marked invalid.
Features
label htmlFor, control id, and aria-describedby.aria-invalid, aria-required, disabled on the control.Form.Error only when the field is invalid (override with forceMount).noValidate so the browser doesn’t double-validate.Example
Anatomy
import { Form } from '@wire-ui/react'
<Form.Root onSubmit={handleSubmit}>
<Form.Field name="email" invalid={emailInvalid} required>
<Form.Label>Email</Form.Label>
<Form.Control>
<input type="email" />
</Form.Control>
<Form.Description>We'll send a magic link.</Form.Description>
<Form.Error>Please enter a valid email address.</Form.Error>
</Form.Field>
<button type="submit">Submit</button>
</Form.Root>Validation & error API
Validation in wire-ui Form is bring-your-own: derive an invalid boolean for each field and pass it to Form.Field. The Field is the source of truth — toggling invalid on the Field flips aria-invalid and data-invalid on the wrapped control, and conditionally renders Form.Error.
const emailInvalid = !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
<Form.Field name="email" invalid={emailInvalid} required>
<Form.Label>Email</Form.Label>
<Form.Control>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</Form.Control>
<Form.Error>Please enter a valid email address.</Form.Error>
</Form.Field>By default Form.Error only renders when invalid is true. Pass forceMount to keep it mounted (useful for animations or reserving space):
<Form.Error forceMount className="
data-[state=hidden]:opacity-0
data-[state=visible]:opacity-100
">
Please enter a valid email address.
</Form.Error>For per-rule messages, branch on your own error state:
const error = !value ? 'Required.' : !validFormat ? 'Wrong format.' : null
<Form.Field invalid={!!error}>
<Form.Control><input ... /></Form.Control>
<Form.Error>{error}</Form.Error>
</Form.Field>Combining with wire-ui inputs
Form.Control only clones the first React element child. Pair it with a native <input> for the simple case, or skip Form.Control entirely and wrap a wire-ui field component (Input.Root, Textarea.Root, etc.) which manages its own id/aria internally — Form.Field still provides layout, error rendering, and the data-invalid flag.
<Form.Field name="email" invalid={emailInvalid}>
<Input.Root invalidType={emailInvalid ? 'email' : ''} errorMessage={{ email: '...' }}>
<Input.Label>Email</Input.Label>
<Input.Field type="email" />
<Input.Error />
</Input.Root>
</Form.Field>Using data attributes
[data-invalid] { /* error state */ }
[data-required] { /* required indicator */ }
[data-disabled] { opacity: 0.5; cursor: not-allowed; }Root props
Form.Root accepts all standard <form> props. The element is rendered with noValidate to suppress browser-native validation popups; remove with noValidate={false} if you need them.
Field props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Used as the id prefix and on the control’s name attribute |
invalid | boolean | false | Marks the field as invalid; toggles aria-invalid / data-invalid |
required | boolean | false | Marks the field as required; toggles aria-required / data-required |
disabled | boolean | false | Disables the control; toggles disabled / data-disabled |
Label props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the child element instead of a <label>, merging htmlFor |
Control props
| Prop | Type | Description |
|---|---|---|
children | React.ReactElement | Exactly one element. id, name, aria-*, disabled, and data-* are merged onto it. |
Error props
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | boolean | false | Render even when the surrounding Field is not invalid |
Data attributes
| Attribute | Element | When present |
|---|---|---|
data-invalid | Field, Label, Control | Field is invalid |
data-required | Field, Label, Control | Field is required |
data-disabled | Field, Label, Control | Field is disabled |
Accessibility
Form.Labelautomatically points to the control viahtmlFor= controlid.Form.Controlsetsaria-invalid,aria-required, andaria-describedbythat references both the Description and Error ids when they’re present.Form.Erroris rendered withrole="alert"so screen readers announce it when it appears.Form.RootusesnoValidate— combine withonSubmitto handle validation yourself.
Keyboard Interactions
| Key | Description |
|---|---|
| Tab | Moves focus to the next control. |
| Enter | Submits the form when focus is inside a control (browser default). |