Skip to content
Components

Field

One form field: a label, one control, an optional description and an error. Field owns the ids, so the label names the control and the control is described by its description and error, and it passes required, disabled and invalid to both at once.

Preview

We send receipts to this address.

Props

  • orientation
  • required
  • disabled
  • invalid
  • label
  • description
  • error
  • optional
  • tooltip
import { Field, FieldControl, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm">
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FieldControl>
  <FieldDescription>We send receipts to this address.</FieldDescription>
</Field>

Usage

When to use

  • Any form control with a label, and a description or an error message to go with it.
  • Set required, disabled or invalid once, for the label and the control together.

When not to use

  • Two or more controls under one name, such as a date split into day, month and year. Give each its own Field.
  • A control with no visible label, such as a search box in a toolbar. Name it with aria-label instead.

Variants

orientation

vertical: the label above the control, for most forms. horizontal: the label beside the control in the first third, for settings pages with short values.

  • We send receipts to this address.

    verticaldefault

  • We send receipts to this address.

    horizontal

import { Field, FieldControl, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm">
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FieldControl>
  <FieldDescription>We send receipts to this address.</FieldDescription>
</Field>
<Field orientation="horizontal" className="w-full max-w-sm">
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FieldControl>
  <FieldDescription>We send receipts to this address.</FieldDescription>
</Field>

Examples

Default

We send receipts to this address.

import { Field, FieldControl, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm">
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" placeholder="you@example.com" />
  </FieldControl>
  <FieldDescription>We send receipts to this address.</FieldDescription>
</Field>

Required

import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm" required>
  <FieldLabel>Workspace name</FieldLabel>
  <FieldControl>
    <Input />
  </FieldControl>
</Field>

With an error

The error text marks the input invalid and is read after its name.

Enter an email like you@example.com.

import { Field, FieldControl, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm">
  <FieldLabel>Email</FieldLabel>
  <FieldControl>
    <Input type="email" defaultValue="not-an-email" />
  </FieldControl>
  <FieldError>Enter an email like you@example.com.</FieldError>
</Field>

With a select

FieldControl goes around the trigger, so the label opens the list. name and required go on Select.

Admins can invite people and change billing.

import { Field, FieldControl, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"

<Field className="w-full max-w-sm">
  <FieldLabel>Role</FieldLabel>
  <Select>
    <FieldControl>
      <SelectTrigger>
        <SelectValue placeholder="Choose a role" />
      </SelectTrigger>
    </FieldControl>
    <SelectContent>
      <SelectItem value="viewer">Viewer</SelectItem>
      <SelectItem value="editor">Editor</SelectItem>
      <SelectItem value="admin">Admin</SelectItem>
    </SelectContent>
  </Select>
  <FieldDescription>Admins can invite people and change billing.</FieldDescription>
</Field>

Disabled

Set when the workspace was created.

import { Field, FieldControl, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

<Field className="w-full max-w-sm" disabled>
  <FieldLabel>Region</FieldLabel>
  <FieldControl>
    <Input defaultValue="Europe" />
  </FieldControl>
  <FieldDescription>Set when the workspace was created.</FieldDescription>
</Field>

Do and don't

Do

  • Use Field for every form control that has a label, so the ids are always linked.
  • Write the error as what to do: Enter an email like you@example.com.
  • Stack fields in a column with gap-lg between them.
  • Set required, disabled and invalid on Field, not on the label and the control one by one.

Don't

  • Don't put more than one control in a Field; give each its own.
  • Don't set id on the control inside FieldControl; set it on Field, or the label stops pointing at it.
  • Don't show an error before the user has left the field or submitted the form.
  • Don't use the description for an error; use FieldError.

Props

NameTypeValuesDefaultDescription
orientationenumvertical | horizontalverticalvertical: the label above the control, for most forms. horizontal: the label beside the control in the first third, for settings pages with short values.
idstring--Id of the control, for a label or link elsewhere on the page. Generated when left out.
namestring--Form name, passed to the control. For a Select, set name on Select instead.
requiredboolean-falsePuts the asterisk on the label and sets required on the control.
disabledboolean-falseDims the label and disables the control.
invalidboolean-falseSets aria-invalid on the control. A FieldError with text does it on its own; set invalid only when the error shows somewhere else.

Slots

NameRequiredDescription
labelyesFieldLabel with the name of the field.
controlyesFieldControl around one control: Input, or the SelectTrigger of a Select.
descriptionnoOptional FieldDescription: what to enter, or why it is needed. One short sentence.
errornoOptional FieldError: what is wrong and how to fix it. Render it only while the value is invalid.

Anatomy

PartDescription
rootThe field: a column with the label above the control, or a grid with the label in the first third beside it.
labelFieldLabel: a Label that points at the control and follows the field's required and disabled.
controlFieldControl: wraps the one control (Input, SelectTrigger) and gives it the id, name, state and descriptions. Adds no element.
descriptionFieldDescription: 13px secondary text under the control. Renders only with text.
errorFieldError: 13px danger text under the control. Renders only with text, and then marks the control invalid.

Accessibility

Role: generic

  • FieldLabel points at the control (htmlFor), so a press on the label focuses it and screen readers read the label as its name.
  • FieldControl gives the control the field's id, and aria-describedby with the description and the error, so screen readers read them after the name.
  • A FieldError with text sets aria-invalid on the control. The error is words, so it never relies on the red edge alone.
  • FieldError is not a live region. After a submit, move focus to the first invalid field; its error is read with it.
  • required puts the asterisk on the label and required on the control, which is what screen readers announce.

Install

npx shadcn add https://werewolf-ui.vercel.app/r/field.json

This also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").