Gesso
Forms

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.

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 by htmlFor on 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.

Shown to everyone you invite.

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.

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.

<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.

Notifications

Choose where we can reach you.

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.

PropTypeDefaultWhat 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.
errorsArray<{ 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's aria-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 set aria-invalid on 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-destructive color, 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 htmlFor adds 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

On this page