Components · Forms
Field
Label, control, help and error wired together with the right ARIA.
Validation wiring
Used for sign-in codes only.
Enter a valid email address.
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="[email protected]"
/>
<FieldDescription>Used for sign-in codes only.</FieldDescription>
<FieldError>{error}</FieldError>
</Field>
);
}
<EmailField initial="not-an-email" />Usage examples
Native select
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
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
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 />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
labelprop: 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-requiredanddisabledfrom the Field. aria-describedbyonly 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
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 (
htmlForpoints at the control id). - When validation changes, FieldError registers and unregisters itself, so
aria-describedbyandaria-invalidupdate with it. - FieldError is an
aria-live="polite"region, so a new message is announced. - NativeSelect opens the platform picker; clicking its inline
labelfocuses 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-requiredcarries the meaning. - Select trigger receives
aria-requiredbut notrequired, 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 itlabeloraria-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
nullwhen there is no error, so the wiring updates in place. - Set
invalidon 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
optionalTexton 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: Input and Textarea, the most common Field controls.
- Select: Single-value picker that reads Field context.
- Combobox: Searchable picker that reads Field context.
- SettingsSection: SettingsRow builds on Field for settings pages.
- Switch: The control in a
layout="row"switch row. - SwitchSettingsList: 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
disabledto 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 nodata-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