Field

User Interface (UI)

How a field looks, from its parts and where they go around each kind of control to its sizes, states and tokens.

Let's start with how a field looks: its parts, where they go around each kind of control, its sizes, how it shows each state and the tokens it reads.

Anatomy

  1. Container — The root <div>, which lays out the parts and carries the size. A Radio Group gets none, since it lays out its own.
  2. Label — The <label>, linked to the control with for, which names it. A Checkbox and the groups name themselves, so they don't get one.
  3. Hint — Optional. Help people need before they fill the control in, like the format you expect, which describes it.
  4. Control — The one control in the Field, which keeps its own parts, and gets the Field's id, description and states.
  5. Error — Only there while the control has an error. It says what's wrong and how to fix it, and describes the control too.

Every field has a container and a control. The label, hint and error are the Field's own parts, and their order depends on the control, which you'll find in the DOM contract.

Controls

What the Field renders depends on the control inside it, since each kind of control is named and laid out in its own way. The props stay the same for all of them:

ControlLabelHint and error
Input, Textarea or a native <input>, <textarea> or <select>The Field's, above the controlThe hint between the label and the control, the error after it
CheckboxIts own, after its boxAfter it, lined up with its label
SwitchThe Field's, before it on the same rowUnder the row
Checkbox GroupIts own legendAfter the group
Radio GroupIts own legendIts own, above its options, from the Field's props

Text controls

We'll only use it to send your receipt

Enter your email address

<Field
  id="email"
  label="Email address"
  hint="We'll only use it to send your receipt"
  error="Enter your email address"
  required
>
  <Input type="email" autoComplete="email" />
</Field>

We recommend putting the label above a text control, with the hint between them, as the Input does. People read both before they type, and they stay in view when the page is zoomed in or the screen is narrow, where a label beside its control gets pushed off to the side. The error goes after the control, right next to the value it's about.

A sentence or two about you, for your profile

200 characters left
<Field id="bio" label="Bio" hint="A sentence or two about you, for your profile">
  <Textarea limit={200} />
</Field>

A Textarea with a limit shows its count right under the box, before the Field's error, and adds it to its description after the Field's hint and error, so people hear what the field asks for before how many characters they have left.

The Field takes a native <input>, <textarea> or <select> as a text control too. A native checkbox or radio isn't one, since a label above it would read as a heading rather than its name, so use the Checkbox or the Radio Group, which name themselves. The Field warns you in development when it gets one.

Checkboxes

You can read them before you sign up

Agree to the terms to create an account

<Field
  id="terms"
  hint="You can read them before you sign up"
  error="Agree to the terms to create an account"
  required
>
  <Checkbox name="terms">I agree to the terms</Checkbox>
</Field>

A Checkbox's label is part of it, after its box, so a click on the text toggles it. That's where people expect it, and where the WAI forms tutorial puts it, so the Field doesn't render a second label, which would leave the checkbox with two. Its hint and error go after it, set in by the width of the box and its gap (--field--indent), so they line up with its label and read as part of the same option.

Switches

We'll email you when the price drops

<Field id="alerts" label="Price alerts" hint="We'll email you when the price drops">
  <Switch />
</Field>

A switch goes after its label, on the same row, with the hint and the error under both. It's how settings lists are laid out, with the labels down one side and the switches down the other, and Material Design pairs every switch with a label on its row for the same reason (Material Design 3). The row uses logical properties, so the switch moves to the left in right-to-left layouts.

Groups

How can we contact you?

Pick at least one

Choose how we can contact you

<Field id="contact" hint="Pick at least one" error="Choose how we can contact you">
  <CheckboxGroup name="contact" options={contact}>How can we contact you?</CheckboxGroup>
</Field>

A group of checkboxes or radios asks one question, and its <legend> asks it, so the legend names the group the way a label names a single control. The Field doesn't render a label for either group, and warns you in development if you give it one. Around a Checkbox Group, it renders the hint and the error after the group and links them to its <fieldset>, which passes the invalid state on to every option.

Delivery

Express arrives the next working day

Choose a delivery option

<Field
  hint="Express arrives the next working day"
  error="Choose a delivery option"
  required
>
  <RadioGroup name="delivery" legend="Delivery">
    <Radio value="standard">Standard</Radio>
    <Radio value="express">Express</Radio>
  </RadioGroup>
</Field>

A Radio Group already renders its own legend, hint and error, with the error above its options, so people read what's wrong before they choose again. Around one, the Field renders nothing of its own. It hands the group its hint, error, required and disabled, and the group puts them where it always does. If the group has a hint or an error of its own as well, the Field's wins, so it's best to set each one in only one place.

Either way, the Field's hint and error look the same as the Radio Group's, with the same text size, colour and spacing, so a form can mix single controls and groups without them looking out of place.

Sizes

The size prop sets the text size of the label, hint and error, and goes to the control too, so the two always match. A native control keeps its own size, which counts characters rather than naming a size. Without one, the Field takes the control's own size, and md is the default. The text sizes and gap are the Radio Group's, which are in turn the Input's.

As it's written on your card

As it's written on your card

As it's written on your card

As it's written on your card

As it's written on your card

<Field id="name-xs" size="xs" label="Full name" hint="As it's written on your card">
  <Input autoComplete="name" />
</Field>
{/* …sm, md, lg, xl */}
SizeTextGapCheckbox indent
xs12px4px20px
sm14px4px24px
md14px4px26px
lg16px4px28px
xl16px4px34px

The checkbox indent is the Checkbox's box plus its gap at the same size, which is also the Checkbox Group's indent under a select-all box. All of them are set in rem, with a line height of 1.25, so they grow with the text when people make it larger.

States

The Field has no look of its own for each state. It sets the state on the control, which already has one, so what people see and what assistive technologies report come from the same attribute.

Required, which screen readers announce
Optional, in words

You can't change it

Disabled

Enter an email address in the correct format, like name@example.com

Invalid
<Field id="name" label="Full name" required>
  <Input autoComplete="name" />
</Field>

<Field id="company" label="Company (optional)">
  <Input autoComplete="organization" />
</Field>

<Field id="username" label="Username" hint="You can't change it" disabled>
  <Input defaultValue="ada" />
</Field>

<Field
  id="email"
  label="Email address"
  error="Enter an email address in the correct format, like name@example.com"
>
  <Input type="email" autoComplete="email" defaultValue="ada@" />
</Field>
StateSet on the controlRecommended look
RequiredThe native requiredNo marker of its own, with "(optional)" on the fields that aren't
DisabledThe control's own disabledThe control's own, and the label at 50% opacity
Invalidaria-invalid="true", with the error in its descriptionThe control's own, and the error in --color--error-text

The label dims when the control itself is disabled, which the Field reads from the control's own :disabled rather than from a prop, so the label can't look disabled while the control isn't. A disabled button in an Input's actions doesn't count.

Required has no look at all, since which fields to mark is a decision about the whole form, as you'll find under Mark what's required. The error message is text rather than a colour alone, so it means the same to people who can't tell red apart (WCAG 1.4.1).

None of the states move the control. The error goes after a single control, so showing it never pushes the control, or anything above it, out of the way while people are looking at it.

Tokens

The field reads two theme tokens, which every component shares:

TokenUsed for
--color--neutral-textThe label, and mixed into the hint at 70%
--color--error-textThe error message

The hint is the text colour at 70%, the same mix as the Input's placeholder and the Radio Group's hint, which needs 4.5:1 against the page (WCAG 1.4.3). With our reference tokens, from the Tailwind CSS palette, it comes to 5.6:1 on white and 7.4:1 on zinc-900. --color--error-text passes 4.5:1 on both too.

If you change the tokens, make sure to check the same pairs. You'll find the tokens themselves in the Button's tokens.css, which you can copy into your theme.

If you need to tune a field, you can set its own variables, such as --field--gap or --field--indent, from a class. The Field's styles sit in the components cascade layer, so your class wins without any specificity tricks:

SignUp.tsx
import styles from "./SignUp.module.css";

export function SignUp() {
  return (
    <Field id="email" label="Email address" className={styles.roomy}>
      <Input type="email" autoComplete="email" />
    </Field>
  );
}
SignUp.module.css
.roomy {
  --field--gap: 0.5rem;
}
VariableWhat it setsmd
--field--gapThe space between the label, the hint, the control and the error0.25rem
--field--font-sizeThe text size of the label, the hint and the error0.875rem
--field--column-gapThe space between a Switch and its label1rem
--field--indentHow far a Checkbox's hint and error sit in1.625rem

UI Checklist

RuleLevelScopeRequirementBasisCheck
field/label-positionShouldComponentThe label sits above a text control, with the hint between them, before a Switch on the same row, and after a Checkbox's boxOpen ComponentsUnit test
field/error-afterShouldComponentThe error comes after a single control, so showing it never moves the controlOpen ComponentsUnit test
field/text-contrastMustComponentThe label, the hint and the error have a contrast of 4.5:1 against the pageWCAG 1.4.3 (AA)axe color-contrast, contrast checker
field/error-in-textMustBothAn error is a message in text, never a colour aloneWCAG 1.4.1 (A)Review
field/like-groupsShouldComponentThe hint and the error have the same text size, colour and spacing as the Radio Group'sOpen ComponentsReview
field/checkbox-indentShouldComponentA Checkbox's hint and error line up with its label, rather than its boxOpen ComponentsVisual regression test
field/resizableMustComponentIts text and spacing are set in rem, so they grow with the text size rather than clipOpen Components, beyond WCAG 1.4.4, 1.4.12 (AA)200% zoom
field/forced-colorsMustComponentA disabled label stays visible in forced colours modeOpen ComponentsEmulation
Copyright © 2026