FormItem
FormItem wraps a label, a control, an optional description and an optional
message. 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 FormItem from '@zuilib/components/form-item'
import Label from '@zuilib/components/label'
import Description from '@zuilib/components/description'
import Message from '@zuilib/components/message'
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
Description and Message ids join the control's aria-describedby.
Invalid
invalid sets data-invalid on the item and its Label / Description /
Message, and becomes the default invalid of every control inside, so the
control gets aria-invalid and the destructive border from one prop.
Message accepts a string or any { message?: string } object, so a form
library's field error can be passed straight through.
Disabled
disabled disables every Headless UI control inside (they read it from the
Field) and sets data-disabled on the item and its parts. Leave it unset to
inherit from an enclosing Fieldset disabled.
With a toggle
Any Headless UI control joins the Field: a Checkbox, Switch, Select,
Listbox or Combobox is labelled and described the same way.
Props
FormItemProps extends HTMLAttributes<HTMLDivElement> with:
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | — | Label, control, Description, Message in any order |
disabled | boolean | inherited | Disables every Headless UI control inside and sets data-disabled. Unset inherits Fieldset disabled; an explicit false overrides it |
invalid | boolean | false | data-invalid on the item and its parts; the default invalid of every control inside |
className | string | — | Merged last after space-y-2 |
The module also exports useFormItem() and FormItemContext, which give a
custom control { disabled, invalid } of the enclosing item (null outside
one), and the FormItemContextValue type.
import {useFormItem} from '@zuilib/components/form-item'
function ColourPicker(props) {
const item = useFormItem()
const invalid = props.invalid ?? item?.invalid ?? false
// ...
}
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="form-item"] | div | Root. data-invalid when invalid, data-disabled from Headless UI when disabled |
[data-slot="label"] | label | From Label; mirrors the item's data-invalid / data-disabled. Parts: label-required, label-dirty |
[data-slot="description"] | p | From Description; same mirrored state |
[data-slot="message"] | p | From Message; same mirrored state |
/* Consumer CSS: tighter field spacing */
[data-slot="form-item"] > * + * {
margin-top: calc(var(--spacing) * 1);
}
Accessibility
- Renders a Headless UI
Field: the control gets a generatedid, theLabela matchingfor(and the controlaria-labelledby), and eachDescription/Messageid is appended to the control'saria-describedby. invalidpropagates to the control asaria-invalid="true";Messagerenders withrole="alert"(passrole="status"for a polite announcement).disabledis applied by Headless UI to every control inside, including aFieldset disabledfurther up.- A control that sets its own
idinside aFormItemneeds a matchinghtmlForon theLabel(set both or neither). - Outside a
FormItem,Label,DescriptionandMessageare plain elements: associate them withhtmlFor/id/aria-describedbyyourself.