Label

A label names a control. Click it and the control responds, and it dims along with a disabled control.

import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

export default function LabelDemo() {
  return (
    <>
      <div className="flex w-56 flex-col gap-2">
        <Label htmlFor="email">Email</Label>
        <Input id="email" type="email" placeholder="you@example.com" />
      </div>
      <Label>
        <Checkbox defaultChecked />
        Subscribe to updates
      </Label>
      <Label>
        <Checkbox disabled />
        Disabled option
      </Label>
      <div className="flex w-56 flex-col gap-2">
        <Label htmlFor="locked" disabled>
          Locked field
        </Label>
        <Input id="locked" placeholder="Disabled" disabled />
      </div>
    </>
  );
}

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 { cx } from "@/lib/cx";

// A label for a control. Use htmlFor to point at an input, or wrap the control inside it so
// clicking the text toggles it. It dims itself when it wraps a disabled control, or when you
// pass `disabled` for a control that sits next to it.
const base = [
  "inline-flex items-center gap-2 text-base font-medium text-text select-none",
  "data-[disabled]:cursor-not-allowed data-[disabled]:text-text-secondary",
  "has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:text-text-secondary",
  "has-[:disabled]:cursor-not-allowed has-[:disabled]:text-text-secondary",
].join(" ");

export function Label({
  disabled,
  className,
  ...props
}: ComponentProps<"label"> & { disabled?: boolean }) {
  return <label data-disabled={disabled ? "" : undefined} className={cx(base, className)} {...props} />;
}

Props

PropTypeDefaultWhat it does
htmlForstring—The id of the input this label names. Not needed when the control is inside the label.
disabledbooleanfalseDims the label. Use it for a disabled control that sits next to the label, not inside it.
classNamestring—Extra classes, added after the label's own.

Every other prop goes to the native label element.

The thinking

A label is the simplest component here, and the one most often skipped. It is just a native label, styled once.

  • A real label: Because it is the native element, clicking it focuses or toggles its control with no script.
  • Two ways to connect: Point htmlFor at an input's id, or wrap the control inside the label, which suits a checkbox, a switch or a radio.
  • It dims itself: Wrap a disabled control and the label dims with no prop. For a control beside it, pass disabled.

Interface design

  • Text: 16px medium in the main text color.
  • With a control inside: The control and the text sit in a row, 8px apart.
  • Disabled: The secondary text color and a not-allowed cursor.

Interaction

  • Click: Clicking the text focuses its input or toggles its checkbox, switch or radio.
  • No selection: The text can't be selected, so a quick double click on a label doesn't highlight it.
  • No states of its own: A label has no hover, focus or pressed state. The control it names shows those.

Empathy

  • It is the name: A label gives its control the accessible name that a screen reader speaks.
  • Keep it visible: Don't hide the label and rely on a placeholder. The label stays when someone starts typing.
  • A bigger target: Wrapping a small control, like a checkbox, in a label makes the whole row clickable.