Field#

The wiring hub for a labelled form control. Field.Root mints one set of ids and publishes them, together with its disabled / invalid / required flags, through a context that every zero form control reads — so a Field.Label names the control, a Field.Description and Field.Error describe it, and the flags land on the control's own anatomy, without a single id written by hand.

Import#

TSX
import { Field } from '@sigx/zero/field';

Field is a compound: Field.Root, Field.Label, Field.Description, Field.Error. It is also re-exported from the @sigx/zero root, together with fieldAnatomy. The context the controls read is a behavior, exported as useFieldContext from @sigx/zero/behaviors.

Usage#

TSX
import { component } from 'sigx';
import { Field } from '@sigx/zero/field';
import { Input } from '@sigx/zero/input';

const EmailField = component(({ signal }) => {
    const state = signal({ email: '', error: '' });

    return () => (
        <Field.Root invalid={!!state.error} required>
            <Field.Label>Email</Field.Label>
            <Input.Root model={() => state.email} type="email" name="email">
                <Input.Control>
                    <Input.Input placeholder="you@example.com" />
                </Input.Control>
            </Input.Root>
            <Field.Description>We never share it.</Field.Description>
            {state.error ? <Field.Error>{state.error}</Field.Error> : null}
        </Field.Root>
    );
});

The Input.Root inside carries no invalid or required of its own — it adopts the field's. Its <input> takes the field's control id, so the Field.Label for lands on it, and its aria-describedby points at the description and error ids. Input.Label is optional inside a Field for the same reason.

Wrapping a selection control#

TSX
<Field.Root disabled={busy()}>
    <Field.Label>Notifications</Field.Label>
    <Switch.Root model={() => state.enabled}>Email me about new replies</Switch.Root>
    <Field.Description>You can change this at any time.</Field.Description>
</Field.Root>

A Checkbox, Switch or RadioGroup inside a Field disables, invalidates and requires with it. A control's own prop wins when set; the field supplies the rest — every consuming control derives each flag as !!props.x || field.x().

A Switch or Checkbox is a <label> of its input too, and an accessible name concatenates every label a control has — so name it once: Field.Label with a Switch.Root that has no children of its own, or the Switch's own text and no Field.Label. See Accessibility.

A compact field#

TSX
<Field.Root size="xs">
    <Field.Label visuallyHidden>Filter rows</Field.Label>
    <Input.Root model={() => state.filter}>
        <Input.Control>
            <Input.Input />
        </Input.Control>
    </Input.Root>
</Field.Root>

A control with no size of its own renders its Field's, the same way it adopts the Field's flags: Input, Textarea, Checkbox, Switch, RadioGroup, Slider, RatingGroup, NumberInput, Combobox, Select, ToggleGroup and FileUpload all do. The control's own size still wins. Only size is inherited — a Field's color accents its label, while a control's colour is its own checked or focus fill. With Field.Label visuallyHidden, which keeps the label as the control's accessible name while it is off screen, that is the whole compact field.

Standalone controls#

A control outside any Field.Root reads an inert context and wires its own ids: Input, Textarea and NumberInput mint an id for their <input> and their own Label, and Checkbox and Switch are <label> elements already. Field is the composition for the description-and-error case, not a requirement.

Anatomy#

PartElementStatesFlagsNotes
rootdiv—disabled, invalid, requiredCarries the variant axes. Provides the field context.
labellabel—disabled, invalid, requiredfor the field's control id; carries the field's label id. Offers visuallyHidden. Inside root.
descriptionp——Carries the description id, referenced by the control's aria-describedby. Inside root.
errorp—invalidrole="alert"; carries the error id. Inside root.

Every part carries data-scope="field" and data-part="<part>". The ids come from one SSR-safe base: <base>-control, <base>-label, <base>-desc and <base>-error. The control's aria-describedby always names both the description and the error id, so a Field.Error rendered later is announced without any re-wiring — the role="alert" on it announces the message the moment it appears. See The anatomy contract.

The context#

Field.Root provides a FieldContext — inert (true only on the fallback a standalone control reads), ids (control, label, description, error), the disabled() / invalid() / required() accessors, size() (the Field's size, undefined on the fallback) and describedBy(), the space-separated description and error ids. A control built on createFormControl joins the size fallback by spreading axisAttrs() in place of variantAttrs(props). An ecosystem control that wants to sit inside a Field reads it through useFieldContext and adopts the ids and flags exactly as the built-in controls do.

Which controls consume it#

The controls that read the field context are Checkbox, Combobox, FileUpload, Input, NativeSelect, NumberInput, RadioGroup, RatingGroup, Select, Slider, Switch and Textarea. Each hands the field's control id to the element the label should name — the visible <input> for Input and NumberInput, the <textarea>, the hidden native input for Checkbox and Switch, the <select> for NativeSelect, the combobox <input>, the trigger <button> for Select and FileUpload, and the control for Slider and RatingGroup — so Field.Label names all of them, an options-driven Select's generated trigger included. RadioGroup.Root instead points its aria-labelledby at the field's label id, since a group has no single control to name.

Props#

Field.Root#

PropTypeDefaultDescription
disabledbooleanfalseRenders data-disabled on root and label; every consuming control inside disables.
invalidbooleanfalseRenders data-invalid on root, label and error; consuming controls render data-invalid and aria-invalid.
requiredbooleanfalseRenders data-required on root and label; consuming controls render required.
color / size / variant / axes / modsdesign-system vocabulary—The variant axes, rendered as data-* on root. A control inside with no size of its own renders the Field's.
classstring—Extra classes on the root element.

Field has no model: it holds no value. The value lives on the control inside it.

Field.Label, Field.Description, Field.Error#

class, and the forwarded HTML attributes except id (the control points at each one's id; Field.Error also refuses role, which is its alert). Each renders its part around its children with the field's id on it. Field.Label also takes visuallyHidden (boolean), which renders data-visually-hidden and keeps the label as the control's accessible name.

In the shipped design systems#

Both @sigx/zero-basic and @sigx/zero-daisyui wire color (the eight recommended roles) and size (xs–xl) on field, so <Field.Root color="error" size="sm"> is styled in both — typically as the label and helper text colour, with the control inside keeping its own colour and taking the Field's size unless it sets one. Neither wires a variant or any mods on the scope; under a design system's /register import those props are therefore absent. See Typed vocabulary.

Input · Textarea · Number Input · Checkbox · Switch · Radio Group · Accessibility