Choice
A reusable indicator-left / content-right layout for a single choice: a control (checkbox, radio, switch) beside an emphasized title and an optional de-emphasized description. Use it to pair a Checkbox or RadioGroup option with a rich, titled label, or reach for it directly to build your own choice-like rows.
Choice is a pure layout + association primitive. It owns the control's id and the description's id and wires them together, but it does not own the control's value: you drop your own control into Choice.Indicator. Pick Choice.Label (a real <label>) when the Choice owns the labeling, or Choice.Title (label-less text) when an ancestor already does.
Get notified by email when a deploy finishes.
When to use
- A checkbox, radio, or switch that needs an emphasized title with a supplementary description beneath it.
- A fully-clickable row (e.g. a list item) where a control sits beside titled text.
- Anywhere you'd otherwise hand-roll the "control + label + helper text" flexbox and
htmlFor/aria-describedbywiring.
When not to use
- Any layout that isn't a labeled checkbox or radio.
Choiceis specifically for choice-like rows: a checkbox or radio input with an associated label and optional description. For general content layout (icon + text, thumbnails, etc.), useMediaObjector a plain flex / grid. - A bare control with a single-line caption: use
Labelwith the control directly. - A field row with no focusable control (a read-only value): use
Field.LabelText.
Inside a Field
Drop Choice.Root inside a Field.Control and the field's id / name / aria-invalid and description/error aria-describedby flow onto the control automatically: the field's aria-describedby is merged with the Choice's own description rather than replaced. Because the control adopts the field's id, Choice.Label's htmlFor resolves to that same control, so the title stays a real, clickable label (clicking Email below toggles the checkbox), sharing the control with Field.Label. Reach for Choice.Title only when you specifically want the title to not be a label (e.g. a clickable row that owns the click itself).
Sent to your primary address.
Choose how we reach you.
Choice.Label vs Choice.Title
Both render the emphasized title with identical typography; they differ in who labels the control:
Choice.Labelrenders a real<label htmlFor>: the title is the control's accessible name, and clicking it toggles the control. Use it for a standalone control (noField.Label, no clickable row).Choice.Titlerenders label-less text (a<p>): use it when an ancestor already owns the labeling and click target, such as a fully-clickable row or a Headless radio item. This avoids nesting two<label>s or splitting the click target. (Inside aFieldyou usually still wantChoice.Label, since it merges with the field's id and stays clickable.)
The example below is a fully-clickable row: the row's own <label> (sharing the control's id) owns the click, so the title is a Choice.Title.
Composition
Polymorphism
Choice.Content, Choice.Title, and Choice.Description each accept asChild to render as your own element while keeping the styling and wiring. For example, render the description as a <div> so it can contain block content. Choice.Label is the mantle Label (it always renders a real <label>, like Field.Label); reach for Choice.Title (which accepts asChild) when you need a different element. Choice.Indicator is itself a slot: it clones whatever single control element you place inside it, injecting the shared id / name / aria-* / disabled.
Accessibility
- The control placed in
Choice.Indicatorreceives the generatedid;Choice.Label'shtmlForand the description'saria-describedbyresolve to it without manual wiring. Choice.Descriptionassociates viaaria-describedbyonly; it is never a second label. A click on it forwards toChoice.Label, so the whole content column toggles the control while the accessible name stays the label alone.- Use exactly one of
Choice.Label/Choice.Titleper choice.Choice.Titleis label-less text for when an ancestor (clickable row, radio item, orField.Label) carries the accessible name, so labels never nest. - Inside a
RadioGroupitem,Choice.TitleandChoice.Descriptionregister their ids with the radio. The option'saria-labelledbynames it by the title alone, and itsaria-describedbypoints at the description. - Inside a
Field, the control adopts the field'sidand itsaria-describedbyis merged (never duplicated) with the field's description and error ids.
API Reference
Choice.Root
The layout and id/association owner. Renders a <div>. When composed inside a Field.Control, the field's id / name / aria-* take precedence over these props.
All props from div (except id, typed below), plus:
Choice.Indicator
The left control slot. Renders a one-line-tall (h-lh) <span> and clones its single element child with the shared id / name / aria-describedby / aria-invalid / aria-errormessage / disabled.
All props from span.
Choice.Content
The right column that stacks the title over the description. Renders a <div>.
All props from div, plus:
Choice.Label
The title as a real <label htmlFor> wired to the control. Use when the Choice owns the labeling. This is the mantle Label (not a re-implementation), so it keeps all of its styling and behavior — mirroring how Field.Label reuses Label.
All props from Label / label, except htmlFor and disabled — those are owned by Choice.Root (the control id and the choice's disabled state).
Choice.Title
The title as label-less text. Renders a <p>. Use when an ancestor (clickable row, radio item, or Field.Label) owns the control's labeling. Inside a RadioGroup item, it names the radio through aria-labelledby; the id defaults to a generated one.
All props from p, plus:
Choice.Description
The supplementary description, wired to the control via aria-describedby. Renders a <p>. A click on it forwards to the sibling Choice.Label, so the description extends the label's click target. The forward skips a click on a link or other interactive content inside the description. After it forwards, it marks the click handled with preventDefault(), so a click-to-activate ancestor (a SelectableList row) does not toggle the control a second time. When the choice uses Choice.Title, the description forwards nothing, because an ancestor owns that click. Inside a RadioGroup item, it describes the radio through aria-describedby.
All props from p except id, which Choice.Root owns so the control's aria-describedby always resolves, plus: