Field
The label, description, and error around a form control, with rows for checkboxes and fieldsets for groups.
Field puts a label, a line of description, and an error around a form control such as
Input, Textarea,
Select, or Checkbox. It
renders no control of its own and wires nothing for you, so you tie the label to the control
with htmlFor and point the control's aria-describedby at the description and the error.
It also lays out checkbox and switch rows, and groups related fields in a fieldset.
We will use this for account updates.
Enter a display name.
Usage
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from '@/components/ui/field';
import { Input } from '@/components/ui/input';
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
type="email"
aria-invalid={Boolean(error)}
aria-describedby={error ? 'email-help email-error' : 'email-help'}
/>
<FieldDescription id="email-help">We only send account updates.</FieldDescription>
<FieldError id="email-error">{error}</FieldError>
</Field>Field stacks its children in a column 6px apart. FieldLabel is a <label> in font-sans,
FieldDescription is a line of text-fg-muted, and FieldError is a line of
text-destructive, all at 14px, and the label steps up to 16px from the md breakpoint.
FieldTitle is a 14px <div>, for text that sits over a control without naming it.
Wire the label, description, and error
Field passes nothing to its children, with no context and no generated ids, so three attributes on the control do the wiring.
id, matched byhtmlForon the FieldLabel. The label then names the control, and a click on it focuses an input or toggles a checkbox or switch.aria-describedby, listing the ids of the FieldDescription and the FieldError, so a screen reader reads them after the control's name. Add the error's id while the error shows.aria-invalid, set while the error shows. A screen reader announces the control as invalid, and Input, Textarea, Select, Checkbox, Radio group, and Switch draw a destructive border for it.
How an error is announced
FieldError renders a <p> with role="alert", and renders nothing at all while it has no
text, so it can stay in place and show up when you hand it a message. A screen reader reads
an alert as soon as it appears, without moving focus, and reads the message again with the
control through aria-describedby. FieldError does not move focus or set aria-invalid, so
after a submit that fails, set both yourself and move focus to the first invalid control.
Save with the field empty to see it.
errors takes an array of objects with a message, the shape a form library or schema
validator returns, and renders those messages in place of the children. It skips entries
without a message, renders nothing when none are left, and shows a message that repeats only
once. One message is text in the <p>, and several are a bulleted list in a <div> with the
same role="alert", since a list cannot sit inside a paragraph.
- Use at least 12 characters.
- Add a number.
Checkbox and switch rows
For a Checkbox or Switch with a label and a line of detail, put the control first in a
FieldRow align="start" and stack the label, description, and error in a FieldContent. A
20px Checkbox or default Switch then lines up with the first line of the label, and
htmlFor makes the label part of the hit target. Wire aria-describedby and aria-invalid
on the control the same way as on any other field. A one-line label needs no FieldContent,
and the default align="center" centers the control on it. A radio item takes the same row,
as Radio group shows.
Adds a pixel so the report can count opens.
Everyone on it opted in, and can unsubscribe.
Confirm permission before sending.
<Field>
<FieldRow align="start">
<Checkbox
id="consent"
checked={consent}
onCheckedChange={(next) => setConsent(next === true)}
aria-invalid={Boolean(error)}
aria-describedby={error ? 'consent-help consent-error' : 'consent-help'}
/>
<FieldContent>
<FieldLabel htmlFor="consent">I have permission to email this list</FieldLabel>
<FieldDescription id="consent-help">Everyone on it opted in.</FieldDescription>
<FieldError id="consent-error">{error}</FieldError>
</FieldContent>
</FieldRow>
</Field>FieldRow sets its children in a row 8px apart, and FieldContent stacks its own 6px apart.
Group fields in a fieldset
FieldSet is a <fieldset>, and a FieldLegend as its first child names the group for
assistive tech. Use one for controls that answer one question, like the channels a person
can be reached on. The legend is text-lg by default, and variant="label" drops it to
14px for a group that reads as one field, such as a radio group. Set disabled on the
FieldSet to disable every control in it, and each Field dims its label to match.
FieldSet spaces its children 24px apart. FieldGroup stacks fields 16px apart without
grouping them for assistive tech, and it is the container a responsive Field measures.
FieldSeparator draws a 1px bg-border line between groups.
Label above or beside
orientation on Field sets where the label goes. vertical, the default, stacks it above
the control. horizontal puts the label first in a row, 16px from the control and aligned
to its top. Beside a full-width control the label shrinks to its longest word, so give it a
width like w-32 shrink-0, which also lines the controls up down a form, and add pt-2.5
to set it on the text line of a 40px Input.
responsive stacks the label until the FieldGroup around it is 32rem wide, and then sets it
beside the control, so the layout follows the room the form has rather than the window.
FieldGroup is the container it measures, so a responsive Field outside one stays stacked.
Narrow the window to see the demo stack.
<FieldGroup>
<Field orientation="responsive">
<FieldLabel htmlFor="name" className="w-32 shrink-0">
Name
</FieldLabel>
<Input id="name" />
</Field>
</FieldGroup>Props
Each part takes the props of the element it renders, FieldLabel takes Radix Label's, and
FieldSeparator takes Separator's, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "responsive" | "vertical" | On Field. Puts the label above the control, beside it, or above it until the FieldGroup around it is 32rem wide. |
align | "center" | "start" | "center" | On FieldRow. Centers the control on a one-line label, or lines it up with the first line of a label that has text under it. |
variant | "legend" | "label" | "legend" | On FieldLegend. legend is text-lg, and label is 14px. |
errors | Array<{ message?: string } | undefined> | On FieldError. Renders these messages in place of the children, skipping any without a message and showing a repeat once. |
Accessibility
- Field adds no role, ids, or ARIA of its own. A label names its control only through
htmlFor, and a description or error reaches a screen reader only through the control'saria-describedby, so the label beside a control you do not wire does not name it. - FieldLabel renders a native
<label>, so a click on it focuses an Input or Textarea and toggles a Checkbox or Switch. A double click on the label does not select its text. - FieldError has
role="alert", so its message is announced when it appears, without moving focus. It does not setaria-invalidon the control or move focus to it. - FieldSet is a native
<fieldset>, and its first FieldLegend names the group. FieldGroup, FieldRow, and FieldContent are plain<div>s that group nothing for assistive tech, and FieldTitle names nothing. - FieldSeparator is decorative (
role="none"), so assistive tech skips it. - A Field that holds a disabled control dims its labels to 64% and shows a not-allowed cursor over them. The description and error keep their color.
- The error's only styling is its
text-destructivecolor, which forced colors repaints as ordinary text, so write the message to say what is wrong. - Field adds nothing to a control's size, and a label tied with
htmlForadds its own area to the control's hit target. - Nothing in Field animates. An error appears and goes without a transition.
When to use something else
- Section for a titled block of a page outside a form. Its title and description use FieldLegend's and FieldDescription's type, so a form's groups and a page's sections match.
- Description list for labels and values that are read rather than edited.
- Cell fields to edit a value inside a table cell, where a label over each control would not fit.
Install
shadcn add @gesso/field