# Field

A labelled form row: label, control, and the one message below it — and, in row layout, the settings row.

- Group: Forms
- Import: `import { Field } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/field/
- Related: input, select, switch

## When to reach for it

Any labelled control. layout="row" is the settings row — label and description at the inline start, control at the inline end — which is a layout here rather than a second component, because the label wiring, the required marker and the message slot are the same three things either way.

## Anatomy

- **Label** — label, rendered as a Radix <Label> carrying htmlFor and an id. The id is what a trigger names itself from, alongside its own value, and what a group points back at — neither of which htmlFor can do.
- **Required mark** — The --danger asterisk after the label when required. aria-hidden, but still inside the label’s text, so the accessible name ends “Email *”.
- **Control slot** (required) — children — ONE element, which the field clones to add id, aria-describedby, aria-required and aria-invalid, and which each control forwards to whatever element carries its role: the trigger for Select, Combobox and DatePicker, the root for a group, the thumb for a Slider. This is the whole contract; everything else is layout.
- **Description** — description, a second line under the LABEL explaining what the setting does — as distinct from hint, which sits under the control and belongs to the input. It has its own id and joins aria-describedby ahead of the message, so a row with both announces both.
- **Message** — A single <p> below the control: error if there is one, hint otherwise, never both. It owns the id that aria-describedby points at, and it is --danger or --ink-3-aa accordingly.
- **Row layout** — layout="row": the label and description in a column at the inline start, the control at the inline end, the message underneath both. The two columns are TOP-aligned — items-start on the block axis, not items-center — so a two-line description does not drag the switch down to the middle of the paragraph, and a column of settings rows keeps every control on the same line as the words that name it.

## Best practices

### Do

- Make children the control itself, not a layout wrapper around it: the wiring is a cloneElement on the single child, so a <div> in between takes the id and the aria-describedby and the label ends up naming a box.
- Let error carry the invalid state. The field sets aria-invalid on the control, and Input, Textarea, NativeSelect, Select and Combobox all read either spelling through isInvalid, so passing invalid as well states the same fact twice from two places that can disagree.
- Pass htmlFor, and the same id on the control, whenever something outside the row has to address it — a form library, a scroll-to-first-error, a test. The generated id is a useId value nothing else can predict.
- Build a settings screen out of layout="row" with description, not out of three hand-rolled divs. The label still reaches the control through the same wiring, so the switch on the far side of the row is named by the words on the near side.

### Don’t

- required here is aria-required and an asterisk, and nothing else: it never reaches the control’s own required attribute, so the browser will not block the submit and the row stays unmarked until you pass error yourself.
- The words above a RadioGroup or a ToggleGroup name it but do not click through. Both roots are <div role="radiogroup">, which htmlFor does not bind to, so the label is pointed AT by the group instead — a reader who clicks it the way they click "Email" gets nothing, exactly as with a <legend>.
- Do not reach for description as a second hint. It explains the SETTING and sits under the label; hint explains the INPUT and sits under the control. A row that uses one for the other reads correctly and lands in the wrong place.
- required is announced on every control here except DatePicker, whose trigger is a plain <button> — a role with nowhere to put aria-required. There the asterisk is the whole of the marking, and a screen reader meets an ordinary optional field.

## Accessibility

- Generates an id when none is given, so the label always points at something.
- Wires aria-describedby, aria-required and aria-invalid onto the control, so validation is announced and not merely drawn.
- hint and error are one slot: when a field is wrong, the thing to read is what is wrong with it.
- description joins aria-describedby ahead of the message, so a settings row announces what the setting does and then what is wrong with it.
- Every control forwards the wiring to the element that carries its role, so the hint under a Select or a Slider is announced and not merely drawn.
- The row layout moves the label to the other side of the row and changes nothing about the association, so a settings row with a Switch or a Select in it is named by the words on the near side. Slider is the exception, and the layout cannot fix it: role="slider" is on the THUMB while the field’s label points at the roleless root, so there the label prop on the Slider is still the only name a reader hears.

## Field

A labelled form row: label, control, and the one message below it. The visible message is only half of accessible validation — it must also reach the control. This wires `aria-describedby`, `aria-required` and `aria-invalid` onto the single control child so the requirement and the error are announced, not merely drawn (WCAG 1.3.1 / 3.3.1 / 4.1.2). An earlier version derived the message id from `htmlFor`, which meant a caller who left `htmlFor` off got a hint that was rendered and never announced — the failure was invisible in the browser and total for a screen reader. The id is now generated when it is not supplied. `hint` and `error` are one slot, not two stacked messages: when a field is wrong, the thing to read is what is wrong with it. `description` is a different slot again — it explains the SETTING, not the input, and it is what `layout="row"` puts under the label to make a settings row. **How the wiring reaches the control.** `cloneElement` puts the four attributes on the single child, and each control forwards them to whatever element carries the role — which is the child itself for `Input`, `Textarea`, `NativeSelect`, `Checkbox`, `Switch` and any host element written by hand, and a trigger, a group or a thumb further down for `Select`, `Combobox`, `DatePicker`, `Slider`, `RadioGroup` and `ToggleGroup`. The composites used to drop them on the floor, which drew a hint under a control that never announced it; a wrapper that appears to wire things up and does not is worse than one that never claimed to. The label's own id travels separately, through context, because a name is the one thing a prop cannot carry: a trigger whose text is its VALUE is named by the label AND by itself, so `<Field label="Region"><Select/></Field>` announces "Region, Australia" rather than either half. Three things stay out of reach, and each of them is the control's own markup rather than a gap in this wiring. A `<label for>` binds only to a labellable element, so the words do not click through to a `RadioGroup`, a `ToggleGroup` or a `Slider`: the first two are a `role="radiogroup"` named by pointing back at the label instead, exactly as a `<legend>` is, and the third carries `role="slider"` on a thumb below a roleless root. `required` reaches a control as `aria-required`, which `DatePicker`'s plain `<button>` trigger and a multiple-value `ToggleGroup`'s `role="toolbar"` have nowhere to put; there the asterisk is the only marker. And `aria-invalid` reaches `Slider`'s root rather than its thumb, so an errored slider is drawn wrong without being announced wrong. Each composite still takes its own `label` prop — that is what names it standing outside a field, and `Select`, `Combobox` and `DatePicker` warn when it is blank. It is no longer used INSTEAD of this one's. What no wiring can reach, the field says out loud in development rather than failing silently: `FIELD_CONTROL_NOT_LABELLABLE` when the child is a host element a label cannot bind to — the `<div>` wrapper that takes the id and leaves the control inside it with nothing — and `FIELD_CONTROL_NOT_WIRED` when there is no single element to wire at all.

### Props

- `label` — `ReactNode`. Visible label text; renders a `--danger` asterisk when `required`.
- `htmlFor` — `string`. The control's `id`. Optional: when omitted, the field generates one and puts it on the control child itself, so the label still points at something. Pass it explicitly when the id has to be stable across renders — a form library referencing it by name, say.
- `description` — `ReactNode`. A second line under the LABEL, explaining what the setting does. Distinct from `hint`, which sits under the control and belongs to the input — "We never share it". This belongs to the thing being switched on, and it is what makes a settings row a settings row. Both reach the control through `aria-describedby`, so a row that has a description and an error announces both.
- `hint` — `ReactNode`. Helper copy shown below the control when there is no `error`.
- `error` — `ReactNode`. Validation message; takes precedence over `hint` when present.
- `required` — `boolean`.
- `layout` — `FieldLayout` default `'stacked'`. Where the control sits relative to its label. See FieldLayout.
- `children` (required) — `ReactNode`.

Also accepts: `HTMLAttributes<HTMLDivElement>`.

## Example — states

```tsx
import { Field, Input, Select, SelectItem, Textarea } from '@misoto22/folio'

<div className="flex w-full max-w-md flex-col gap-5">
  <Field label="Email" required hint="We never share it.">
    <Input type="email" placeholder="you@example.com" />
  </Field>
  <Field label="Full name" error="Name is required.">
    <Input />
  </Field>
  <Field label="Region">
    <Select label="Region" defaultValue="au">
      <SelectItem value="au">Australia</SelectItem>
      <SelectItem value="nz">New Zealand</SelectItem>
      <SelectItem value="jp">Japan</SelectItem>
    </Select>
  </Field>
  <Field label="Notes" hint="Markdown is fine.">
    <Textarea rows={3} placeholder="Anything else?" />
  </Field>
</div>
```

## Example — one message slot

```tsx
import { Field, Input } from '@misoto22/folio'

<Field
  label="Work email"
  required
  hint="The receipt goes here — use the address on the invoice."
  error={wrong ? 'Enter a full address, like maya.chen@studio.example.' : undefined}
  className="w-full max-w-sm"
>
  <Input
    type="email"
    autoComplete="email"
    value={email}
    onChange={(event) => setEmail(event.target.value)}
  />
</Field>
```

## Example — naming a group

```tsx
import { Field, RadioGroup, RadioGroupItem } from '@misoto22/folio'

<Field
  label="Delivery speed"
  hint="Weekend delivery is metro only."
  className="w-full max-w-sm"
>
  <RadioGroup defaultValue="standard" aria-label="Delivery speed">
    <RadioGroupItem value="standard">Standard — 3 to 5 business days</RadioGroupItem>
    <RadioGroupItem value="express">Express — next business day</RadioGroupItem>
    <RadioGroupItem value="pickup">Collect from the Newtown store</RadioGroupItem>
  </RadioGroup>
</Field>
```

## Example — a stable id

```tsx
import { Button, Field, Input } from '@misoto22/folio'

<div className="flex w-full max-w-sm flex-col gap-4">
  <Field label="Email" htmlFor="checkout-email" hint="The receipt goes here.">
    <Input
      id="checkout-email"
      type="email"
      autoComplete="email"
      defaultValue="maya.chen@studio.example"
    />
  </Field>
  <Field label="Postcode" htmlFor="checkout-postcode" error="Enter a four-digit postcode.">
    <Input
      id="checkout-postcode"
      inputMode="numeric"
      autoComplete="postal-code"
      defaultValue="20o0"
    />
  </Field>
  <Button
    variant="secondary"
    className="self-start"
    onClick={() => document.getElementById('checkout-postcode')?.focus()}
  >
    Jump to the first error
  </Button>
</div>
```
