Overview
Use Field to wrap a form control with a label, description, validation message, and disabled or invalid state. Field owns the wiring that is easy to get wrong by hand: the label is associated with the control automatically, the description and error are attached to the control through aria-describedby, and aria-invalid is set for you when validation fails.
Field keeps form layout consistent and helps connect supporting text to the control. Compose it with any control: an input, textarea, select, checkbox, radio group, slider, switch, combobox, or a custom element passed through the render prop.
Anatomy
A field usually includes a label, control, description, and error message. FieldHelperSlot lets description and error occupy the same layout space, FieldGroup stacks related fields, and FieldSeparator divides a long form without creating a new semantic group.
The root renders a div laid out as a column. The control renders an input wrapped in a full-width span; pass the render prop to substitute any element, which drops the wrapper span. Descriptions render as paragraphs and errors as divs, both sized down and colored through theme tokens. All field parts receive data attributes for their state — data-invalid, data-disabled, data-focused — so styles react to the field rather than to props you thread by hand.
Behavior
Validation timing. validationMode defaults to "onSubmit". With Honest UI's native Form wrapper there is no Base UI Form context driving submit-time validation, so validation commits happen when the user presses Enter in the control, on blur when the mode is "onBlur", and on every change when the mode is "onChange" (optionally debounced with validationDebounceTime). Validate on submission or after a field has been edited; avoid showing an error before a person has had a chance to act.
Custom rules. Pass a validate function that receives the value and all form values and returns an error string, an array of strings, or null. Async functions are supported. Native constraint violations such as required or type="email" surface their browser messages automatically.
External ownership. The invalid, dirty, and touched props let a form library drive state instead. Setting invalid colors the control, sets aria-invalid, and makes a matching FieldError render immediately.
Keep entered values after failure and write each message as a specific recovery instruction.
Accessibility
The label is a real <label> associated with the control automatically; clicking anywhere in it focuses the control, which gives checkboxes and radios a much larger activation target. You do not need htmlFor or matching ids inside a Field.
While a description is rendered, its id is attached to the control's aria-describedby. A rendered error joins the same list, and the control receives aria-invalid="true", so screen readers announce the failure state and the instructions together when focus moves to the field. An unrendered FieldError contributes nothing to the accessibility tree, which is why errors appear only when they are true.
Keyboard users reach the control with Tab and edit with normal text keys; pressing Enter commits validation. The control stands 28 px (sm), 32 px (default), or 40 px (lg) tall, clearing the WCAG 2.2 minimum target size of 24 px; leave extra room around fields that are primary touch targets on mobile.
Colors come from theme tokens, so descriptions, errors, borders, and disabled dimming adapt to dark mode automatically. Control padding uses logical properties, so the layout mirrors in right-to-left locales. The helper slot reserves one space for description and error, so swapping between them does not shift the surrounding form, and its transitions are wrapped in motion-safe.
Installation
Usage
import {
Field,
FieldControl,
FieldDescription,
FieldError,
FieldHelperSlot,
FieldLabel,
} from "@/components/ui/field";<Field>
<FieldLabel>Name</FieldLabel>
<FieldControl name="name" placeholder="Enter your name" />
<FieldHelperSlot>
<FieldDescription>Visible on your public profile.</FieldDescription>
<FieldError />
</FieldHelperSlot>
</Field>A bare <FieldError /> renders the current validation message — from native constraints or your validate function — and renders nothing while the field is valid. To show an application-owned message, pass children and set invalid yourself, as shown in Custom validity output.
Don't do this
Expecting an error to appear on submit
// Bad
<Form onSubmit={save}>
<Field name="email">
<FieldLabel>Email address</FieldLabel>
<FieldControl name="email" type="email" required />
<FieldError>Enter an email address.</FieldError>
</Field>
</Form>// Good
<Form onSubmit={save}>
<Field name="email" validationMode="onBlur">
<FieldLabel>Email address</FieldLabel>
<FieldControl name="email" type="email" required />
<FieldError />
</Field>
</Form>With the default "onSubmit" mode and a native Form, nothing re-validates when the submit button is clicked — the browser shows its own bubble for native constraints, and custom messages never display. Pick a mode that matches when validation should actually run, or drive the field with the invalid prop from your own submission logic.
Replacing the description with the error
// Bad
<Field invalid={hasError}>
<FieldLabel>Password</FieldLabel>
<FieldControl type="password" />
{hasError ? (
<FieldError>Use at least 12 characters.</FieldError>
) : (
<FieldDescription>Use at least 12 characters.</FieldDescription>
)}
</Field>// Good
<Field invalid={hasError}>
<FieldLabel>Password</FieldLabel>
<FieldControl type="password" />
<FieldHelperSlot>
<FieldDescription>Use at least 12 characters.</FieldDescription>
<FieldError>Choose a longer password.</FieldError>
</FieldHelperSlot>
</Field>Swapping one message for another removes the requirement at the exact moment someone needs to reread it, and the two texts rarely say the same thing. The helper slot keeps both in the accessibility tree's rotation and reserves their shared space, so nothing shifts when the error appears.
Marking a field required but disabled
// Bad
<Field>
<FieldLabel>VAT number *</FieldLabel>
<FieldControl name="vat" required disabled={!isBusiness} />
</Field>// Good
{isBusiness ? (
<Field>
<FieldLabel>
VAT number <span className="text-destructive-foreground">*</span>
</FieldLabel>
<FieldControl name="vat" required />
</Field>
) : null}Disabled controls are skipped by keyboard focus, dimmed, and excluded from submission, so a required marker on them is either a lie (the form submits fine without it) or a trap (the field cannot be filled). Render the field only when it applies, or leave it enabled with a description explaining when it matters.
Examples
These examples cover the Field states and relationships that are independent of a specific control. For composition with Autocomplete, Checkbox, Combobox, Number Field, Radio Group, Select, Slider, Switch, or Textarea, use that control's page.
Required field
The asterisk marks the field visually; the required attribute enforces it and announces it.
Disabled field
Disabling the root dims the label and description too, so the whole unit reads as inactive.
Native validation error
The example starts with an incomplete address. Pressing Enter commits validation and shows the associated error; clicking Validate email triggers the browser's native message.
Custom validity output
FieldValidity hands your render function the raw validity data, useful for debugging or bespoke messaging.
Password instructions
A description states the rule before anyone types.
Complete form
Fields composed with Select, Checkbox, pending-free submission, and an aria-live status line.
Custom validate function
Return a string from validate to fail the field with your own message; return null to pass it.
import {
Field,
FieldControl,
FieldDescription,
FieldError,
FieldHelperSlot,
FieldLabel,
} from "@/components/ui/field"
import { Form } from "@/components/ui/form"
const RESERVED_NAMES = ["admin", "root", "support"]
export default function FieldValidate() {
return (
<Form className="w-full max-w-xs">
<Field
name="username"
validationMode="onBlur"
validate={(value) => {
const username = String(value ?? "").toLowerCase()
if (RESERVED_NAMES.includes(username)) {
return "That username is reserved. Try another."
}
return null
}}
>
<FieldLabel>Username</FieldLabel>
<FieldControl minLength={3} placeholder="e.g. aurora-dev" />
<FieldHelperSlot>
<FieldDescription>
At least 3 characters. Names like “admin” are taken.
</FieldDescription>
<FieldError />
</FieldHelperSlot>
</Field>
</Form>
)
}
API reference
All parts forward their matching Base UI Field props plus the render composition prop. Field accepts:
FieldControl adds Honest UI's size: "sm" (28 px), "default" (32 px), "lg" (40 px), or a number of characters. FieldError accepts match: pass true to always render its children, or a ValidityState key such as "typeMismatch" to gate visibility. FieldHelperSlot, FieldGroup, and FieldSeparator accept native div props; only the group and separator change layout, none of them add semantics.
See the Base UI Field API for validation internals and control composition.