Field

A field joins a label, a control, a hint and an error message, and keeps them connected for assistive tech. A fieldset groups related controls under one name.

Shown on your profile.

A sentence or two.

Notify me about
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldControl, FieldDescription, FieldError, FieldLabel, Fieldset, FieldsetLegend } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";

export default function FieldDemo() {
  return (
    <>
      <div className="flex w-full max-w-xs flex-col gap-6">
        <Field name="name">
          <FieldLabel>Name</FieldLabel>
          <Input placeholder="Njoh Simplice" />
          <FieldDescription>Shown on your profile.</FieldDescription>
        </Field>
        <Field name="email" validationMode="onBlur">
          <FieldLabel>Email</FieldLabel>
          <Input type="email" required placeholder="you@example.com" />
          <FieldError match="valueMissing">Enter your email address.</FieldError>
          <FieldError match="typeMismatch">That doesn&apos;t look like an email address.</FieldError>
        </Field>
        <Field name="bio">
          <FieldLabel>Bio</FieldLabel>
          <FieldControl render={<Textarea />} />
          <FieldDescription>A sentence or two.</FieldDescription>
        </Field>
        <Field name="handle" disabled>
          <FieldLabel>Handle</FieldLabel>
          <Input placeholder="Disabled" />
        </Field>
      </div>
      <Fieldset>
        <FieldsetLegend>Notify me about</FieldsetLegend>
        <Label>
          <Checkbox defaultChecked />
          Replies
        </Label>
        <Label>
          <Checkbox />
          Mentions
        </Label>
      </Fieldset>
    </>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { Field as BaseField } from "@base-ui/react/field";
import { Fieldset as BaseFieldset } from "@base-ui/react/fieldset";
import { cx } from "@/lib/cx";

type WithClass<T> = Omit<T, "className"> & { className?: string };

// A Field joins a label, a control, a hint and an error message. Base UI connects them: clicking
// the label focuses the control, and the hint and error are tied to it for assistive tech.
// Input works inside a Field with no extra code. For any other control, such as a Textarea,
// render it through FieldControl: <FieldControl render={<Textarea />} />.
export function Field({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Root>>) {
  return <BaseField.Root className={cx("flex w-full flex-col gap-2", className)} {...props} />;
}

export function FieldLabel({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Label>>) {
  return (
    <BaseField.Label
      className={cx("text-base font-medium text-text data-[disabled]:text-text-secondary", className)}
      {...props}
    />
  );
}

export function FieldDescription({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Description>>) {
  return <BaseField.Description className={cx("text-sm text-text/70", className)} {...props} />;
}

// Shown when the control is invalid. `match` picks which problem it is for, such as
// valueMissing or typeMismatch, or pass true to show it whenever the field is invalid.
export function FieldError({ className, ...props }: WithClass<ComponentProps<typeof BaseField.Error>>) {
  return <BaseField.Error className={cx("text-sm text-danger", className)} {...props} />;
}

export const FieldControl = BaseField.Control;

// A group of related controls under one legend. It is a native fieldset that is named by its
// legend (through aria-labelledby), so a screen reader announces the legend as the group's name.
export function Fieldset({ className, ...props }: WithClass<ComponentProps<typeof BaseFieldset.Root>>) {
  return <BaseFieldset.Root className={cx("flex flex-col gap-3", className)} {...props} />;
}

export function FieldsetLegend({ className, ...props }: WithClass<ComponentProps<typeof BaseFieldset.Legend>>) {
  return <BaseFieldset.Legend className={cx("text-base font-semibold text-text", className)} {...props} />;
}

Props

PropTypeDefaultWhat it does
namestring—On Field. The name of the control, used when the field is part of a form.
disabledbooleanfalseOn Field. Disables the control and dims the label.
invalidboolean—On Field. Marks the control invalid yourself, instead of waiting for validation.
validationModeonSubmit | onBlur | onChangeonSubmitOn Field. When the control is checked: on submit, when focus leaves, or on every change.
matchstring | boolean—On FieldError. Which problem it is for, such as valueMissing or typeMismatch, or true to show it for any.
renderelement—On FieldControl. The control to connect, such as <Textarea />.

Field, FieldLabel, FieldDescription, FieldError, Fieldset and FieldsetLegend pass every other prop to the matching Base UI part.

The thinking

Every form control needs a name, a hint when the rule isn't obvious, and a clear message when it goes wrong. A field keeps those three attached to the control.

  • Built on Base UI: Base UI links the label to the control and ties the hint and the error to it, so none of that wiring is written by hand.
  • Input just works: Put an Input inside a Field and it connects on its own. For another control, such as a Textarea, render it through FieldControl.
  • Fieldset for groups: When several controls answer one question, like a set of checkboxes, wrap them in a Fieldset with a legend.

Interface design

  • Layout: A column with 8px between the label, the control and the message under it.
  • Label: 16px medium in the main text color. It dims when the field is disabled.
  • Hint and error: Both 14px. The hint is the main text color at 70%, 7.9:1 against the page. The error is the danger color, 5.5:1.
  • Legend: 16px semibold, above the grouped controls.

Interaction

  • Label click: Clicking a field's label focuses its control.
  • Validation timing: The Email field checks itself when focus leaves it, once you have typed in it. Type something that isn't an email, or type and then delete it, and the matching error appears. Fix it and the error goes away. A field you haven't typed in doesn't complain when you tab past it.
  • Invalid state: While an error shows, the control gets the danger border.
  • Disabled: The control can't be focused or edited, and the label dims.

Empathy

  • Connected, not just near: The hint and the error are tied to the control with aria-describedby, so a screen reader reads them with the field and not as stray text.
  • Say how to fix it: Write an error that tells people what to do, like Enter your email address, not just Invalid.
  • A group has a name: A Fieldset is a native fieldset named by its legend, so a screen reader announces Notify me about as the name of the group of checkboxes.