Label

A bare <label> primitive for forms. Wires a control via htmlFor so clicking the text focuses the bound input, reads its typography from the --cb-* token system, and exposes a single indicator prop so the required asterisk and the (optional) suffix paint identically everywhere.

Preview

Installation

npx shadcn@latest add https://craftbits.dev/r/label.json

Usage

Minimal — bind the label to a control by id:

import { Label } from "@craft-bits/core";
 
<Label htmlFor="email">Email</Label>
<input id="email" type="email" />

Required — paint the asterisk and announce (required) to assistive tech:

<Label htmlFor="email" indicator="required">Email</Label>
<input id="email" type="email" required />

Optional — append a muted (optional) suffix:

<Label htmlFor="website" indicator="optional">Website</Label>
<input id="website" type="url" />

Understanding the component

  1. htmlFor is the contract. A label without htmlFor does not focus the control. The prop is forwarded verbatim onto the underlying <label>, so the consumer owns the id and the wiring is one attribute away from native form semantics.
  2. One indicator prop, three identical states. Hand-rolled asterisks drift in colour and weight across a form; the indicator enum collapses the choice to three values so every required field paints the same red * and every optional field paints the same muted (optional) suffix. The asterisk is aria-hidden and paired with a sr-only "(required)" so screen readers do not announce a literal star.
  3. Disabled fades automatically. The label carries peer-disabled:opacity-50 and group-data-[disabled=true]:opacity-50 selectors. Drop it next to a peer <input disabled> or inside a data-disabled group (e.g. <Field.Root disabled>) and it fades without a prop change.
  4. Token-only theming. Text, indicator, and faded states all read from --cb-fg, --cb-fg-muted, and --cb-error. Theme swaps and dark mode repaint without the consumer touching a class.

Props

PropTypeDefaultDescription
htmlForstringId of the bound control. Clicking the label text focuses that element.
indicator"none" | "required" | "optional""none"Visual badge after the label text. "required" paints a red * plus a screen-reader announcement; "optional" paints a muted (optional) suffix.
childrenReactNodeVisible label text.
classNamestringMerged onto the underlying <label>.

Inherits all native <label> attributes via spread.

Accessibility

  • Renders a real <label> element, so screen readers and click-to-focus behave natively.
  • htmlFor is the source of truth for the label/control binding — keep it in sync with the bound id.
  • indicator="required" renders the asterisk with aria-hidden="true" and pairs it with a <span class="sr-only"> (required)</span> so assistive tech announces the requirement instead of the literal star.
  • indicator="optional" renders the (optional) suffix visually (no screen-reader-only announcement is added — the parent control's lack of required already conveys this).
  • Disabled state fades via peer-disabled / group-data-[disabled=true] selectors, so the label tracks the bound control without a redundant disabled prop.

Credits

  • Extracted from: algoflashcards (src/platform/ui/label.tsx). The source wrapped Radix's Label.Root to inherit click-to-focus behaviour for free. craft-bits drops the Radix dependency — a native <label> already handles htmlFor — and broadens the API with an indicator enum so the required asterisk and the (optional) suffix render consistently without per-form copy-paste.