Skip to content

Field

Stable

The layout system for forms — labels, controls, descriptions and errors arranged consistently, accessibly and at any width.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/field.json

Usage

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

  1. 1LabelNames the control. Always visible; associated with htmlFor.
  2. 2DescriptionOptional guidance: format, limits, consequences.
  3. 3ControlAny prfct form control — here an Input.
PartRole
FieldSet / FieldLegendA native <fieldset> and its caption. Groups related controls and names the group for assistive technology.
FieldGroupVertical rhythm between fields — 20px by default, 12px for checkbox groups. Also a container-query root for responsive fields.
FieldOne control with its label, description and error. role="group", laid out vertically, horizontally or responsively.
FieldContentStacks a label and description next to a control in horizontal fields.
FieldLabel / FieldTitleThe control's label; FieldTitle is a non-label heading for choice cards.
FieldDescriptionHelper text below the control.
FieldErrorThe validation message, with an icon, announced as an alert.
FieldSeparatorA divider between sections, with an optional centered label.

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.

Profile

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>
Why both attributes?
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.

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.

Plan

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.

Incident alerts

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.

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-label instead.
  • 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.

Do.Specific: says what’s wrong and how to fix it, with an icon as well as color.

Invalid input

Don’t.Vague, and color is the only signal — invisible to many people.

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. Field renders role="group"; FieldSet and FieldLegend are a native <fieldset> and <legend>, which assistive technology announces as the group's name.
  • Association. Connect every FieldLabel to its control with htmlFor/id, and descriptions and errors with aria-describedby.
  • Errors are alerts. FieldError renders role="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-disabled on the Field and disabled on the control; the label and description dim together.

API reference

Field

Renders a <div role="group">. Accepts all div props.

PropTypeDefault
orientation

Stacks the parts, places them in a row, or switches at the FieldGroup's md container width.

"vertical" | "horizontal" | "responsive""vertical"
data-invalid

Marks the field as invalid for styling and tests. Pair with aria-invalid on the control.

booleanNo default
data-disabled

Dims the label and description. Pair with disabled on the control.

booleanNo default

FieldSet and FieldLegend

FieldSet renders a <fieldset>; FieldLegend renders its <legend>.

PropTypeDefault
variant

FieldLegend only. legend is a 16px section title; label matches a field label for small groups.

"legend" | "label""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.

PropTypeDefault
children

The message. Takes precedence over errors.

ReactNodeNo default
errors

Messages from a validation library. Duplicates are removed; several render as a list.

Array<{ message?: string } | undefined>No default

FieldSeparator

Renders a horizontal rule between sections.

PropTypeDefault
children

Optional label centered on the rule, e.g. “or”.

ReactNodeNo default