Source: https://monarchui.dev/components/form-field/

Components · Forms

# Field

Label, control, help and error wired together with the right ARIA.

@rookcom/monarch-react

Component

## Validation wiring

## Usage examples

### Native select

### Compact toolbar select

### Switch with description

## Purpose

Groups a label, one form control, help text and an error message, and wires their ids and ARIA attributes together so every Monarch control is labelled and described correctly.

### Use when

- Any Monarch form control (Input, Textarea, Select, Checkbox, Switch, Combobox) needs a visible label, help text or validation message.
- A custom control should pick up the same wiring: call `useFieldControl(props)` inside it.
- The platform picker is the better control (long lists on phones, no-JavaScript forms, compact toolbar filters): use NativeSelect.
- A Switch needs a label and description beside it: `layout="row"`.

### Avoid when

- A Checkbox or Switch with its own inline `label` prop: it already labels itself.
- A control labelled only by surrounding context (a search box with `aria-label`).

## Design principles

- Wiring is automatic: the control receives `id`, `aria-describedby`, `aria-invalid`, `aria-required` and `disabled` from the Field.
- `aria-describedby` only names parts that are rendered: FieldDescription and FieldError register themselves.
- Explicit props on the control always win, so controls also work outside a Field.
- FieldError can always be present: it renders nothing when its children are empty.
- NativeSelect is the real `<select>`: it keeps the platform picker, form submission and typeahead, and only restyles the closed control.
- Layout is a prop, not a new component: `layout="row"` rearranges the same parts.

## API reference

```tsx
import { Field, FieldDescription, FieldError, FieldLabel, NativeSelect, useFieldContext, useFieldControl } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `Field` | component | client | `.mn-field-` |
| `FieldDescription` | component | client | `.mn-field-` |
| `FieldError` | component | client | `.mn-field-` |
| `FieldLabel` | component | client | `.mn-field-` |
| `NativeSelect` | component | client | `.mn-field-` |
| `useFieldContext` | hook | client | – |
| `useFieldControl` | hook | client | – |

`Field` renders a `<div class="mn-field">` and provides context; `FieldLabel` forwards its ref to the `<label>`.

`useFieldControl(props, { invalid })` merges Field context into a control’s props; `useFieldContext()` returns the raw context, or null outside a Field.

`NativeSelect` forwards its ref to the `<select>`; all native select attributes pass through. It renders `<span class="mn-native-select">` around the select and its chevron.

Generated from the TypeScript declarations of `packages/react/src/components/forms/form-field`.

## Properties

`Field` `id` overrides the generated control id; the label, description and error ids derive from it (`<id>-label`, `<id>-description`, `<id>-error`).

### `Field`

Groups a label, a control, help text and an error message, and wires their ids together: the control gets `id`, `aria-describedby`, `aria-invalid`, `aria-required` and `disabled` automatically. Descriptions and errors register themselves, so `aria-describedby` only names elements that exist. <Field invalid={!!error}> <FieldLabel>Email</FieldLabel> <Input type="email" /> <FieldError>{error}</FieldError> </Field>

| Prop | Type | Description |
| --- | --- | --- |
| `children` required | `ReactNode` |  |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` | Override the generated control id (e.g. to match an existing label). |
| `invalid` | `boolean` |  |
| `layout` | `"stack" \| "row"` | `stack` (default): label, control, help and error in a column. `row`: label and help on the left, the control on the right, for a Switch (or Checkbox, or a compact NativeSelect) with a description. Exposed as `data-layout="row"`. |
| `required` | `boolean` |  |

### `FieldDescription`

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `FieldError`

Renders nothing when empty, so it can always be present. Announced politely when it appears.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `FieldLabel`

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |
| `optionalText` | `string` | Text shown after the label for optional fields, e.g. "Optional". |

### `NativeSelect`

The platform `<select>`, styled like Input and wired to Field. Use it where the native picker is right: long option lists on mobile, forms that must work without JavaScript, compact toolbar filters. For a styled listbox use Select; for search use Combobox.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` |  |
| `invalid` | `boolean` | Marks the value invalid. Inside a Field, the Field's `invalid` is used. |
| `label` | `ReactNode` | Visible label rendered inline before the select, for toolbars and filter rows outside a Field ("Sort Newest ▾"). Inside a Field use FieldLabel. |
| `options` | `readonly NativeSelectOption[]` | Options as data. `children` (`<option>`, `<optgroup>`) are rendered after them. |
| `placeholder` | `string` | First, empty option shown while no value is chosen. It is disabled once the control is `required`, so it cannot be submitted. |
| `size` | `"sm" \| "md" \| "lg"` | Control height. `sm` is the compact toolbar size (`--mn-control-sm`). Default `md`. |
| `wrapperClassName` | `string` | Class for the wrapper `<span class="mn-native-select">`; `className` goes on the `<select>`. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `FieldLabel optionalText` | `'string'` | Text after the label for optional fields, for example "Optional". Not shown when the Field is `required`, which shows a red asterisk instead. |
| `Field layout` | `'stack'``'row'` | `stack` (default) is a column. `row` puts the label and description on the left and the control on the right, with the error under both: the switch-with-description row. Exposed as `data-layout="row"`. |
| `NativeSelect size` | `'sm'``'md'``'lg'` | Height from `--mn-control-*`, matching Input. `sm` (2rem) is the compact toolbar size. |
| `NativeSelect label` | `'ReactNode'` | Inline visible label before the control, for toolbars outside a Field (`data-inline-label`). Inside a Field, use FieldLabel instead. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Invalid | `data-invalid on the Field, aria-invalid="true" on the control` | The error message is added to `aria-describedby` only while the Field is invalid and FieldError has content. |
| Required | `required and aria-required="true" on the control` | FieldLabel shows an `aria-hidden` asterisk; the requirement is announced from the control. |
| Disabled | `data-disabled on the Field, disabled on the control` | The label switches to the disabled text colour. |
| NativeSelect placeholder | `<option value="" disabled> when required` | `placeholder` adds an empty first option; on a required control it is disabled so it cannot be chosen again. |

## Interaction behaviour

- Clicking the label focuses the control (`htmlFor` points at the control id).
- When validation changes, FieldError registers and unregisters itself, so `aria-describedby` and `aria-invalid` update with it.
- FieldError is an `aria-live="polite"` region, so a new message is announced.
- NativeSelect opens the platform picker; clicking its inline `label` focuses it.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves to the control; Field and its label add no tab stop. |
| Arrow keys, typing, Space / Alt+ArrowDown | NativeSelect: the platform behaviour of `<select>` (change option, typeahead, open the picker). |

Field adds no key handling of its own. Keyboard behaviour is the control’s; the label is reachable by clicking only.

## Accessibility

Role

–

Accessible name

FieldLabel, linked to the control with

htmlFor

.

- The control is described by FieldDescription and, while invalid, FieldError, in that order.
- The required asterisk is `aria-hidden`; `aria-required` carries the meaning.
- Select trigger receives `aria-required` but not `required`, which is invalid on a button.
- Ids come from `useId`, so they are stable between server and client render.
- NativeSelect is a real `<select>` (role combobox or listbox by browser), so screen readers announce it natively. Outside a Field, give it `label` or `aria-label`.
- The chevron is decorative (`aria-hidden`).

## Responsive behaviour

- A vertical stack (label, control, help, error) that fills its container at every width.
- `layout="row"` keeps the control on the right at every width; the text column shrinks and wraps first (`minmax(0, 1fr)`).
- NativeSelect fills a stacked Field; on its own it is inline and shrinks with an ellipsis rather than overflowing.

## Theme support

- Label, help and error colours are semantic tokens (`--mn-color-text-*`, `--mn-color-danger-strong`), so they follow light, dark and more-contrast themes.
- The error colour does not change with the product accent.
- NativeSelect reuses the Input surface, border and focus tokens; the open picker is drawn by the platform and follows `color-scheme`.

## Do and don't

### Do

- Keep FieldError mounted and pass `null` when there is no error, so the wiring updates in place.
- Set `invalid` on the Field, not on the control, when the control is inside a Field.
- Write errors that say how to fix the value ("Enter a valid email address.").

### Don't

- Do not put more than one control in a Field: the label and descriptions point at one id.
- Do not rely on the asterisk alone to explain a required field when most fields are optional; use `optionalText` on the optional ones instead.
- Do not style a raw `<select>` per page: use NativeSelect so it picks up Field wiring and tokens.

## Related components

- [Input](https://monarchui.dev/components/text-field/): Input and Textarea, the most common Field controls.
- [Select](https://monarchui.dev/components/select/): Single-value picker that reads Field context.
- [Combobox](https://monarchui.dev/components/combobox/): Searchable picker that reads Field context.
- [SettingsSection](https://monarchui.dev/components/settings-section/): SettingsRow builds on Field for settings pages.
- [Switch](https://monarchui.dev/components/switch/): The control in a `layout="row"` switch row.
- [SwitchSettingsList](https://monarchui.dev/components/switch-settings-list/): A list of switch rows that each save on their own, with saving and error states.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | None |

## Testing coverage

- Wires label, description and error to the control (required, aria-invalid, accessible description).
- Only references descriptions that exist.
- Adds and removes the error reference as validation changes.
- Propagates `disabled` to the control.
- Works in StrictMode (double effects).
- Text-field tests prove explicit props win outside a Field; Select tests prove a trigger gets aria-required, not required.
- NativeSelect: Field wiring (name, description, error, required), disabled placeholder and options, uncontrolled and controlled changes, inline label naming and focus, Tab focus, disabled from Field.
- `layout="row"` labels and describes a Switch, the label toggles it, and the default stacked markup has no `data-layout`.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/field |

Tests: `packages/react/src/components/forms/form-field`

## All example code

### Validation wiring

```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldError, FieldLabel, Input } from '@rookcom/monarch-react';

function EmailField({ initial = '' }: { initial?: string }) {
  const [v, setV] = useState(initial);
  const error = v.length > 0 && !v.includes('@') ? 'Enter a valid email address.' : null;
  return (
    <Field invalid={!!error} required>
      <FieldLabel>Email</FieldLabel>
      <Input
        type="email"
        value={v}
        onChange={(e) => setV(e.target.value)}
        placeholder="you@example.com"
      />
      <FieldDescription>Used for sign-in codes only.</FieldDescription>
      <FieldError>{error}</FieldError>
    </Field>
  );
}

<EmailField initial="not-an-email" />
```

### Native select

```tsx
import { Field, FieldDescription, FieldLabel, NativeSelect } from '@rookcom/monarch-react';

const TIMEZONES = [
  { value: 'UTC', label: 'UTC' },
  { value: 'Europe/London', label: 'London' },
  { value: 'Asia/Kolkata', label: 'Kolkata' },
  { value: 'America/New_York', label: 'New York' },
];

<Field required>
  <FieldLabel>Time zone</FieldLabel>
  <NativeSelect options={TIMEZONES} placeholder="Choose a time zone" defaultValue="" />
  <FieldDescription>Used for reminders and reports.</FieldDescription>
</Field>
```

### Compact toolbar select

```tsx
import { NativeSelect } from '@rookcom/monarch-react';

const SORTS = [
  { value: 'updated', label: 'Last updated' },
  { value: 'created', label: 'Newest' },
  { value: 'priority', label: 'Priority' },
];

<NativeSelect size="sm" label="Sort" options={SORTS} defaultValue="updated" />
```

### Switch with description

```tsx
import { useState } from 'react';
import { Field, FieldDescription, FieldLabel, Switch } from '@rookcom/monarch-react';

function DigestRow() {
  const [on, setOn] = useState(true);
  return (
    <Field layout="row">
      <FieldLabel>Weekly digest</FieldLabel>
      <FieldDescription>A summary of activity, sent on Monday mornings.</FieldDescription>
      <Switch checked={on} onCheckedChange={setOn} />
    </Field>
  );
}

<DigestRow />
```
