Field
Labels, descriptions and errors for a form control, plus Videorc's SettingsRow and the grouped settings list. A Field names and describes its control with no ids to wire.
Installation
$ pnpm dlx shadcn@latest add @videorc/fieldThis brings field-context, the module every form control reads to find its Field.
Usage
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
SettingsRow,
} from "@/components/ui/field";<Field>
<FieldLabel>Stream title</FieldLabel>
<Input placeholder="Untitled livestream" />
<FieldDescription>Shown on YouTube and Twitch.</FieldDescription>
</Field>Anatomy
<FieldSet>
<FieldLegend />
<FieldGroup>
<Field>
<FieldLabel />
{/* the control */}
<FieldDescription />
<FieldError />
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel />
<FieldDescription />
</FieldContent>
{/* the control */}
</Field>
</FieldGroup>
</FieldSet>
<FieldGroup variant="grouped">
<SettingsRow label="…" description="…" error="…">
{/* the control */}
</SettingsRow>
</FieldGroup>Examples
Settings rows
A settings page is a FieldGroup variant="grouped" of SettingsRows: one inset group split by hairlines, each row with its label and 12 px description on the left and its control on the right. error puts the row's own refusal or failure under the description. A row whose control is a button names its group with aria-label, since a label can't name a button.
Choice cards
Wrap a horizontal Field in a FieldLabel to make the whole card the checkbox's label: a click anywhere toggles it, and the card shows the focus ring. data-slot="checkbox-group" on the FieldGroup tightens the gap.
Theming
| Token or attribute | Meaning |
|---|---|
rounded-row, bg-inset, divide-border | The grouped variant: one inset group split by hairlines |
text-meta, text-muted-foreground | Descriptions, at 12 px in the secondary tier |
text-foreground, text-destructive | An error: monochrome words, the error glyph in the destructive tone |
data-orientation | vertical, horizontal or responsive on a Field |
data-variant | default or grouped on a FieldGroup |
data-settings-row | Marks a SettingsRow's Field |
data-disabled="true" | On a Field, dims its label |
Accessibility
Naming and describing
A Field names its control. Each control and the FieldLabel register an id with the Field when they mount (an explicit id always wins), and then:
- the label points
htmlForat the first control a label element can name: an Input, a Textarea, a Switch or a Checkbox; - a control no label element can point at, such as a Slider thumb or a group of toggles, takes the label through
aria-labelledby, unless it has anaria-labelof its own; - every
FieldDescriptiondescribes the Field's controls througharia-describedby, unless a control passes its own.
Nothing points at an id that isn't on the page, two controls in one Field never share an id, and outside a Field nothing is added. So a SettingsRow's label names its switch and its description describes it, with no ids at all.
A FieldError is a role="alert", so it is announced when it appears. Mark the control aria-invalid too.
Your own controls
The wiring lives in components/ui/field-context.ts, its own module so a control can read it without pulling the Field's styles into its bundle. A control of your own joins in with the same hooks:
"use client";
import type { ComponentProps } from "react";
import {
useFieldControlId,
useFieldDescribedBy,
} from "@/components/ui/field-context";
function ColorWell({
id,
"aria-describedby": ariaDescribedBy,
...props
}: ComponentProps<"input">) {
const controlId = useFieldControlId(id);
const describedBy = useFieldDescribedBy(ariaDescribedBy);
return (
<input
aria-describedby={describedBy}
id={controlId}
type="color"
{...props}
/>
);
}API reference
Each part takes every attribute of the element it renders, plus:
FieldGroup
Prop
Type
Field
Prop
Type
FieldLabel
A Label.
Prop
Type
FieldDescription
Prop
Type
FieldError
Prop
Type
FieldLegend
Prop
Type
SettingsRow
Every Field prop except orientation, plus:
Prop
Type
field-context
Prop
Type