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.jsonUsage
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
- htmlFor is the contract. A label without
htmlFordoes 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. - One
indicatorprop, three identical states. Hand-rolled asterisks drift in colour and weight across a form; theindicatorenum 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 isaria-hiddenand paired with asr-only"(required)" so screen readers do not announce a literal star. - Disabled fades automatically. The label carries
peer-disabled:opacity-50andgroup-data-[disabled=true]:opacity-50selectors. Drop it next to apeer<input disabled>or inside adata-disabledgroup (e.g.<Field.Root disabled>) and it fades without a prop change. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
htmlFor | string | — | Id 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. |
children | ReactNode | — | Visible label text. |
className | string | — | Merged 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. htmlForis the source of truth for the label/control binding — keep it in sync with the boundid.indicator="required"renders the asterisk witharia-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 ofrequiredalready conveys this).- Disabled state fades via
peer-disabled/group-data-[disabled=true]selectors, so the label tracks the bound control without a redundantdisabledprop.
Credits
- Extracted from:
algoflashcards(src/platform/ui/label.tsx). The source wrapped Radix'sLabel.Rootto inherit click-to-focus behaviour for free. craft-bits drops the Radix dependency — a native<label>already handleshtmlFor— and broadens the API with anindicatorenum so the required asterisk and the(optional)suffix render consistently without per-form copy-paste.