Skip to main content

Field

Field wraps a label, a control, an optional description and an optional error. It renders Headless UI's Field, which generates the control id and wires for, aria-labelledby and aria-describedby between the parts, so no ids are needed. Its disabled and invalid reach every ZUI control inside through context.

import Field from '@zuilib/primitives/field'
import Label from '@zuilib/primitives/label'
import FieldDescription from '@zuilib/primitives/field-description'
import FieldError from '@zuilib/primitives/field-error'

Basic

Put the parts in any order; the wiring is by role. The control receives the field's id, the Label its for, and the FieldDescription and FieldError ids join the control's aria-describedby.

Loading example

Invalid

invalid sets data-invalid on the field and its Label / FieldDescription / FieldError, and becomes the default invalid of every control inside, so the control gets aria-invalid and the danger border from one prop. FieldError accepts a string or any { message?: string } object, so a form library's field error can be passed straight through.

Loading example

Disabled

disabled disables every control inside (they read it from the field) and sets data-disabled on the field and its parts. Leave it unset to inherit from an enclosing Fieldset disabled.

Loading example

With a toggle

Any ZUI control joins the field: a Checkbox, Switch, NativeSelect, Select or Combobox is labelled and described the same way.

Loading example

Props

FieldProps extends HTMLAttributes<HTMLDivElement> with:

PropTypeDefaultDescription
childrenrequiredReactNodeLabel, control, FieldDescription, FieldError in any order
disabledbooleaninheritedDisables every control inside and sets data-disabled. Unset inherits Fieldset disabled; an explicit false overrides it
invalidbooleanfalsedata-invalid on the field and its parts; the default invalid of every control inside
classNamestringMerged last after space-y-2

The module also exports useField() and FieldContext, which give a custom control { disabled, invalid } of the enclosing field (null outside one), and the FieldContextValue type.

import {useField} from '@zuilib/primitives/field'

function ColourPicker(props) {
const field = useField()
const invalid = props.invalid ?? field?.invalid ?? false
// ...
}

Slots

SlotElementNotes
[data-slot="field"]divRoot. data-invalid when invalid, data-disabled when disabled
[data-slot="label"]labelFrom Label; mirrors the field's data-invalid / data-disabled. Parts: label-required, label-unsaved
[data-slot="field-description"]pFrom FieldDescription; same mirrored state
[data-slot="field-error"]pFrom FieldError; same mirrored state
/* Consumer CSS: tighter field spacing */
[data-slot="field"] > * + * {
margin-top: calc(var(--spacing) * 1);
}

Accessibility

  • The control gets a generated id, the Label a matching for (and the control aria-labelledby), and each FieldDescription / FieldError id is appended to the control's aria-describedby.
  • invalid propagates to the control as aria-invalid="true"; FieldError renders with role="alert" (pass role="status" for a polite announcement).
  • disabled is applied to every control inside, including from a Fieldset disabled further up.
  • A control that sets its own id inside a Field needs a matching htmlFor on the Label (set both or neither).
  • Outside a Field, Label, FieldDescription and FieldError are plain elements: associate them with htmlFor / id / aria-describedby yourself.