Field
The accessible scaffolding around a single form control — label, description,
and validation error, all wired together. Built on Base UI Field. Input
already composes Field internally; reach for Field directly when you need
to wrap a custom control or lay out the pieces yourself.
Import
import { Field } from "@photon-ai/pho-ui/components/field";
Basic
Field.Label is associated with Field.Control automatically — no manual
htmlFor. Field.Description adds helper text that stays put.
Used in your project URLs. Lowercase letters and dashes only.
<Field.Root>
<Field.Label>Workspace name</Field.Label>
<Field.Control placeholder="acme-inc" />
<Field.Description>Lowercase letters and dashes only.</Field.Description>
</Field.Root>
Validation
Field.Error appears when the control fails validation. Set validationMode on
Field.Root to choose when that runs (onBlur here), and give each Field.Error
a match to write copy per failure reason — otherwise it shows the browser’s
native message. The control picks up the invalid ring automatically via
data-invalid.
We'll send the invite here.
<Field.Root validationMode="onBlur">
<Field.Label>Work email</Field.Label>
<Field.Control type="email" required placeholder="[email protected]" />
<Field.Description>We'll send the invite here.</Field.Description>
<Field.Error match="valueMissing">Enter your work email.</Field.Error>
<Field.Error match="typeMismatch">
That doesn't look like an email address.
</Field.Error>
</Field.Root>
Anatomy
- Field.Root — owns the field state and the disabled / invalid wiring
- Field.Label — auto-associated
<label>(no manualhtmlFor) - Field.Control — the
<input>; swap the element withrender - Field.Description — persistent helper text
- Field.Error — validation message, shown only when the control is invalid
- Field.Item — wraps one option’s control + label in a checkbox / radio group
- Field.Validity — render-prop access to the raw
ValidityState
Props
- Field.Root
- disabled —
boolean(defaultfalse) — disable the control; takes precedence overdisabledonField.Control - name —
string— the field’s form name; takes precedence overnameon the control - invalid —
boolean— force the invalid state (for externally controlled validity) - validate —
(value, formValues) => string | string[] | null | Promise<…>— custom validator; return message(s) ornull - validationMode —
onSubmit·onBlur·onChange(defaultonSubmit) — when validation runs - validationDebounceTime —
number(default0) — debounce, in ms, foronChangevalidation - dirty / touched —
boolean— override the tracked interaction state for external control - actionsRef —
RefObject— imperative handle exposingvalidate()
- disabled —
- Field.Label
- nativeLabel —
boolean(defaulttrue) — setfalsewhenrendering a non-<label>element (e.g. a<button>control) to skip native label behaviors
- nativeLabel —
- Field.Control
- onValueChange —
(value: string, details) => void— fires on value change when controlled - defaultValue — the uncontrolled initial value
- …native
<input>attributes —type,value,required,placeholder, … forwarded to the element
- onValueChange —
- Field.Error
- match —
true | keyof ValidityState— always show, or show only for a specific validity reason (valueMissing,typeMismatch, …); omit to match any invalid state
- match —
- Field.Item
- disabled —
boolean(defaultfalse) — disable this item’s control;Field.Root’sdisabledwins
- disabled —
- Field.Validity
- children —
(state) => ReactNode— render prop given the field’s validity, error, and value
- children —
All parts forward className and their native element attributes.
Accessibility
- Labelling —
Field.Labelrenders a real<label>and the control getsaria-labelledbypointed at it, so the two are linked without a manualhtmlFor/id; clicking the label focuses the control. - Descriptions —
Field.DescriptionandField.Erroreach register theiridinto the control’saria-describedby, so both the helper text and the active error are announced when the control is read. - Invalid state — when the field is invalid the control receives
aria-invalid="true"anddata-invalid(the ring styling hook). The matchingField.Erroris surfaced througharia-describedbyrather than an assertive live region, so it’s read when the user reaches the control, not shouted mid-typing. - Keyboard / focus —
Fieldadds no interactions of its own; focus and keyboard behavior are whatever the wrapped control provides. The control shows the brand ring onfocus-visible, switching to the error ring while invalid.
Best practices
- Use
Fieldto wrap a non-Inputcontrol (a custom picker, a textarea) so it still gets a label / description / error for free. - Prefer
Field.Descriptionfor always-on guidance andField.Errorfor validation feedback — don’t overload one to do both. - Give each
Field.Erroramatchso the message names the actual problem; fall back to the native message only when a generic one will do. - Pick
validationModedeliberately —onBlurfor most inputs,onChange(with a debounce) only where live feedback earns the noise. - Let Base UI manage the label association; adding your own
htmlFor/idcan desync it.