Field
StableThe layout system for forms — labels, controls, descriptions and errors arranged consistently, accessibly and at any width.
Visible to everyone you invite.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/field.jsonUsage
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field"<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input id="name" />
<FieldDescription>Shown on your public profile.</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>Field works with every prfct control — Input, Textarea, Select, Checkbox, Radio Group, Switch, Slider and more — so every form in a product shares one rhythm.
Anatomy
We’ll send a sign-in link here.
- 1LabelNames the control. Always visible; associated with htmlFor.
- 2DescriptionOptional guidance: format, limits, consequences.
- 3ControlAny prfct form control — here an Input.
Examples
Form
A complete form: a FieldSet with a legend and description, a two-column row for name fields, a select, a textarea, a separator and the actions. Everything inherits the same spacing without a single margin class.
Validation
Mark the Field with data-invalid, the control with aria-invalid, and render a FieldError linked through aria-describedby. Pass a list to errors to show several rules at once. Submit this form to see it in action.
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" aria-invalid aria-describedby="email-error" />
<FieldError id="email-error">Enter an email address like name@company.com.</FieldError>
</Field>aria-invalid on the control is what assistive technology reads and what turns the border red. data-invalid on the Field is a styling and testing hook for the whole field — use it to adjust surrounding elements without reaching into the control.Horizontal
Settings pages read best as rows: the label and description on the left, the control on the right. Use orientation="horizontal" with a FieldContent wrapper.
A summary of activity in your projects, every Monday.
Notify me when someone @mentions me in a comment.
Occasional news about features and improvements.
Choice cards
Wrap a whole Field in a FieldLabel and it becomes a selectable card. The entire card is the click target, and the checked state tints it with the brand color. Works with radio groups and checkboxes alike.
Checkbox group
Group related checkboxes in a FieldSet with a FieldLegend — screen readers announce the legend when focus enters the group. Give the inner FieldGroup data-slot="checkbox-group" for tighter spacing.
Responsive
orientation="responsive" stacks label and control in narrow containers and places them side by side once the surrounding FieldGroup is wider than the md container breakpoint. It responds to its container, not the viewport, so the same form works in a dialog and on a full page.
Serve your docs from your own domain.
Where your data is stored and processed.
Guidelines
When to use
- For every form, from a single inline field to a multi-step flow.
- For settings pages, with horizontal fields.
- For selectable cards that behave like radios or checkboxes.
When not to use
- For inline editing inside tables or toolbars, where a label would be visual noise — label the control with
aria-labelinstead. - For filters and search boxes that sit outside a form — a bare Input Group is usually enough.
One column
Lay out forms in a single column. Multi-column forms break the vertical path the eye follows and are easy to complete out of order. Pair fields side by side only when they belong together, like first and last name, or city and postal code.
Validate, then explain
Validate on submit and on blur, never on the first keystroke. Put the error directly below the field it concerns, say what went wrong and how to fix it, and keep the person's input — don't clear the field. For long forms, also move focus to the first invalid field.
Invalid input
Descriptions earn their place
Add a FieldDescription only when it changes what people do: a format, a limit, a consequence. A description that restates the label is noise. If every field needs one, the labels probably need work.
Accessibility
- Grouping.
Fieldrendersrole="group";FieldSetandFieldLegendare a native<fieldset>and<legend>, which assistive technology announces as the group's name. - Association. Connect every
FieldLabelto its control withhtmlFor/id, and descriptions and errors witharia-describedby. - Errors are alerts.
FieldErrorrendersrole="alert", so it is announced as it appears. When several fields fail at once, consider announcing a single summary instead. - Not by color alone. Invalid fields combine a red border, an icon and a written message (WCAG 1.4.1).
- Big targets. Choice cards make the whole card clickable, far beyond the 24×24px minimum (WCAG 2.5.8).
- Disabled fields. Put
data-disabledon theFieldanddisabledon the control; the label and description dim together.
API reference
Field
Renders a <div role="group">. Accepts all div props.
FieldSet and FieldLegend
FieldSet renders a <fieldset>; FieldLegend renders its <legend>.
FieldGroup
Renders a <div> that stacks fields with 20px gaps and establishes the field-group container for responsive fields. Set data-slot="checkbox-group" for 12px gaps.
FieldLabel, FieldTitle and FieldContent
FieldLabel renders a Label and accepts its props. When it wraps a Field, it becomes a choice card. FieldTitle renders a <div> styled like a label, for card headings that aren't the control's accessible name. FieldContent renders a <div> that stacks a label and description.
FieldDescription
Renders a <p> with helper text at 13px in muted-foreground. Links inside it use the link color.
FieldError
Renders a <div role="alert"> with an alert icon. Renders nothing when it has no content.
FieldSeparator
Renders a horizontal rule between sections.