Skip to Content
⭐️ Leave a star →
ComponentsForm

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

Auto-wires label htmlFor, control id, and aria-describedby.
Toggles aria-invalid, aria-required, disabled on the control.
data-invalid, data-required, data-disabled on Field, Label, and Control.
Renders Form.Error only when the field is invalid (override with forceMount).
Bring-your-own validation — no schema lock-in.
Form Root uses noValidate so the browser doesn’t double-validate.

Example

How should we address you?
We'll send a magic link.
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

PropTypeDefaultDescription
namestringUsed as the id prefix and on the control’s name attribute
invalidbooleanfalseMarks the field as invalid; toggles aria-invalid / data-invalid
requiredbooleanfalseMarks the field as required; toggles aria-required / data-required
disabledbooleanfalseDisables the control; toggles disabled / data-disabled

Label props

PropTypeDefaultDescription
asChildbooleanfalseRender the child element instead of a <label>, merging htmlFor

Control props

PropTypeDescription
childrenReact.ReactElementExactly one element. id, name, aria-*, disabled, and data-* are merged onto it.

Error props

PropTypeDefaultDescription
forceMountbooleanfalseRender even when the surrounding Field is not invalid

Data attributes

AttributeElementWhen present
data-invalidField, Label, ControlField is invalid
data-requiredField, Label, ControlField is required
data-disabledField, Label, ControlField is disabled

Accessibility

  • Form.Label automatically points to the control via htmlFor = control id.
  • Form.Control sets aria-invalid, aria-required, and aria-describedby that references both the Description and Error ids when they’re present.
  • Form.Error is rendered with role="alert" so screen readers announce it when it appears.
  • Form.Root uses noValidate — combine with onSubmit to handle validation yourself.

Keyboard Interactions

KeyDescription
TabMoves focus to the next control.
EnterSubmits the form when focus is inside a control (browser default).
Last updated on

MIT License © 2026 wire-ui

Form – Wire UI