Skip to content
design
Components

Label

The caption of a form control, which focuses the control when pressed.

On this page
Captions for a text field, a number, a long wrapping caption, a select and a switch, each naming its control.
Code
import { Input } from "@bosunkit/components/input";
import { Label } from "@bosunkit/components/label";
import { Select } from "@bosunkit/components/select";
import { Switch } from "@bosunkit/components/switch";
import { Moon, Ruler, Ship } from "lucide-react";
import { useId, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Captions for every kind of control: a text field, a number with an icon in its caption, a long caption that " +
  "wraps, a select trigger, and a switch whose caption sits beside it and turns it on and off.";

const FIELD: CSSProperties = { display: "flex", flexDirection: "column", minWidth: 0 };
const CAPTION_WITH_ICON: CSSProperties = { display: "inline-flex", alignItems: "center", gap: 6 };

export default function Showcase(): ReactElement {
  const id: string = useId();
  const [quietNights, setQuietNights]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);

  return (
    <div
      style={{
        width: "100%",
        maxWidth: 560,
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
        gap: "var(--space-md)",
      }}
    >
      <div style={FIELD}>
        <Label htmlFor={`${id}-name`} style={CAPTION_WITH_ICON}>
          <Ship size={12} aria-hidden="true" />
          Vessel name (required)
        </Label>
        <Input id={`${id}-name`} required defaultValue="Althea" autoComplete="off" />
      </div>
      <div style={FIELD}>
        <Label htmlFor={`${id}-draft`} style={CAPTION_WITH_ICON}>
          <Ruler size={12} aria-hidden="true" />
          Draft (m)
        </Label>
        <Input id={`${id}-draft`} type="number" inputMode="decimal" step={0.05} defaultValue="2.10" />
      </div>
      <div style={FIELD}>
        <Label htmlFor={`${id}-margin`}>
          Margin kept under the keel when Bosun plans a passage through shallow water (m)
        </Label>
        <Input id={`${id}-margin`} type="number" inputMode="decimal" step={0.1} defaultValue="1.5" />
      </div>
      <div style={FIELD}>
        <Label htmlFor={`${id}-units`}>Depth unit</Label>
        <Select.Root defaultValue="m">
          <Select.Trigger id={`${id}-units`}>
            <Select.Value />
          </Select.Trigger>
          <Select.Portal><Select.Positioner alignItemWithTrigger={true} ><Select.Popup ><Select.List>
            <Select.Item value="m">Meters</Select.Item>
            <Select.Item value="ft">Feet</Select.Item>
            <Select.Item value="fm">Fathoms</Select.Item>
          </Select.List></Select.Popup></Select.Positioner></Select.Portal>
        </Select.Root>
      </div>
      <div
        style={{
          gridColumn: "1 / -1",
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          gap: "var(--space-md)",
          paddingTop: "var(--space-sm)",
          borderTop: "1px solid var(--border)",
        }}
      >
        <Label htmlFor={`${id}-quiet`} style={{ ...CAPTION_WITH_ICON, marginBottom: 0, cursor: "pointer" }}>
          <Moon size={12} aria-hidden="true" />
          Quiet nights: voice replies off from 22:00 to 06:00
        </Label>
        <Switch id={`${id}-quiet`} checked={quietNights} onCheckedChange={setQuietNights} />
      </div>
    </div>
  );
}

A label is the caption of a form control. Its htmlFor names the control by id, so a press on the caption focuses the control and a screen reader reads the caption as the control's name. It renders a <label> and takes its attributes; a second press on the caption is prevented, so it does not start a text selection on it.

Examples

Label

A tap on the caption puts the cursor in the field its htmlFor names.
Code
import { Input } from "@bosunkit/components/input";
import { Label as FieldLabel } from "@bosunkit/components/label";
import { useId, type ReactElement } from "react";

export const description: string = "A tap on the caption puts the cursor in the field its htmlFor names.";

export default function Label(): ReactElement {
  const fieldId: string = useId();
  return (
    <div>
      <FieldLabel htmlFor={fieldId}>Vessel name</FieldLabel>
      <Input id={fieldId} />
    </div>
  );
}

Associate the visible name

Give the control a unique id and pass it to htmlFor. A repeated form row needs a different association for every instance. A placeholder is an example of input, not the label; it disappears while the user types.

Keep the label concise and put format instructions in a hint associated through aria-describedby. Use FormField when label, hint and validation need to work together. A group of related controls needs its own legend or group name as well as labels on individual controls. Clicking the caption should focus or activate the intended control, including in an embedded example or dialog.

Props

Also takes every attribute of ComponentProps<"label">.