Radio Group

A Radix-style radio group primitive. A single value flows in, a single onValueChange flows out, and the group owns roving-tabindex plus arrow-key navigation across its items. Use it as a single <RadioGroup options={…} /> for the labelled-list common case, or break it open into <RadioGroup.Root> + <RadioGroup.Item> when you need layout control.

Preview

Installation

npx shadcn@latest add https://craftbits.dev/r/radio-group.json

Usage

Shorthand — pass options[] for the common labelled-list case:

import { RadioGroup } from "@craft-bits/core";
 
const [plan, setPlan] = useState("pro");
 
<RadioGroup
  value={plan}
  onValueChange={setPlan}
  options={[
    { value: "free", label: "Free" },
    { value: "pro", label: "Pro" },
    { value: "team", label: "Team", disabled: true },
  ]}
/>

Compound — break open the group when you need custom layout per row:

<RadioGroup.Root value={tier} onValueChange={setTier}>
  <RadioGroup.Item value="bronze" label="Bronze" />
  <RadioGroup.Item value="silver" label="Silver" />
  <RadioGroup.Item value="gold" label="Gold" />
</RadioGroup.Root>

Uncontrolled — let the group own the value, seed it via defaultValue:

<RadioGroup defaultValue="pro" options={plans} />

Understanding the component

  1. Compound API, Radix-compatible. <RadioGroup.Root> is also re-exported as the bare <RadioGroup> so the shorthand stays a single tag. <RadioGroup.Item> is a real <button role="radio"> with aria-checked and data-state, so screen readers and styling hooks behave identically to Radix.
  2. Roving-tabindex + arrow keys. The group binds keydown and walks the DOM for every enabled button[role="radio"]. Arrow keys cycle through items (wrapping at the ends), Home jumps to the first and End to the last. Only the selected item — or the first item, when none is selected — has tabIndex=0, so Tab lands on the group exactly once.
  3. Controlled + uncontrolled. Pass value for fully controlled use or defaultValue for uncontrolled.
  4. Options shorthand. options[] is sugar for rendering a labelled <RadioGroup.Item> per row. Each option may carry its own description and disabled flag.
  5. Token-only theming. Border, fill, dot colour, and focus halo all read from --cb-* tokens. Theme swaps and dark mode repaint without a prop change.

Props

<RadioGroup> / <RadioGroup.Root>

PropTypeDefaultDescription
valuestringControlled selected value. Pair with onValueChange.
defaultValuestringUncontrolled initial value. Ignored when value is provided.
onValueChange(value: string) => voidFired with the next value on every user selection.
disabledbooleanfalseDisables every item in the group at once.
namestringAdds a hidden mirror <input type="radio"> per item, so the value participates in native form submission.
requiredbooleanfalseSets aria-required and propagates to the hidden mirror inputs when name is set.
optionsRadioGroupOption[]Shorthand: render a labelled <RadioGroup.Item> per option.
classNamestringMerged onto the underlying <div role="radiogroup">.

<RadioGroup.Item>

PropTypeDefaultDescription
valuestringThe value this item represents. Required.
disabledbooleaninherits groupDisables only this item. Falls back to the group's disabled when unset.
labelReactNodeOptional label rendered to the right of the dot. Wired via htmlFor.
descriptionReactNodeOptional helper text rendered under the label.
classNamestringMerged onto the underlying <button role="radio">.

Accessibility

  • The root renders as <div role="radiogroup"> with aria-required and aria-disabled reflecting the group props.
  • Each item renders as <button role="radio"> with aria-checked="true" | "false" and data-state="checked" | "unchecked".
  • Keyboard: Tab moves into the group once. Inside the group, ArrowDown / ArrowRight move to the next enabled item and ArrowUp / ArrowLeft to the previous, wrapping at the ends. Home jumps to the first item and End to the last. Space and Enter select the focused item.
  • When label is provided, a sibling <label htmlFor> is rendered with the same id as the radio button.
  • disabled (group or item) propagates to the button (removed from arrow-key navigation) and the label.
  • required is reflected as aria-required on the radiogroup and propagated to the hidden mirror inputs when name is set.

Credits

  • Extracted from: algoflashcards (src/platform/ui/radio-group.tsx). The source wrapped radix-ui's RadioGroup primitives. craft-bits drops the radix-ui dependency — the group is a plain <div role="radiogroup"> with its own roving-arrow logic — and broadens the API into a compound RadioGroup.{Root,Item} plus an options[] shorthand with per-row label and description slots.