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.jsonUsage
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
- 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">witharia-checkedanddata-state, so screen readers and styling hooks behave identically to Radix. - Roving-tabindex + arrow keys. The group binds
keydownand walks the DOM for every enabledbutton[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 — hastabIndex=0, so Tab lands on the group exactly once. - Controlled + uncontrolled. Pass
valuefor fully controlled use ordefaultValuefor uncontrolled. - Options shorthand.
options[]is sugar for rendering a labelled<RadioGroup.Item>per row. Each option may carry its owndescriptionanddisabledflag. - 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>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled selected value. Pair with onValueChange. |
defaultValue | string | — | Uncontrolled initial value. Ignored when value is provided. |
onValueChange | (value: string) => void | — | Fired with the next value on every user selection. |
disabled | boolean | false | Disables every item in the group at once. |
name | string | — | Adds a hidden mirror <input type="radio"> per item, so the value participates in native form submission. |
required | boolean | false | Sets aria-required and propagates to the hidden mirror inputs when name is set. |
options | RadioGroupOption[] | — | Shorthand: render a labelled <RadioGroup.Item> per option. |
className | string | — | Merged onto the underlying <div role="radiogroup">. |
<RadioGroup.Item>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The value this item represents. Required. |
disabled | boolean | inherits group | Disables only this item. Falls back to the group's disabled when unset. |
label | ReactNode | — | Optional label rendered to the right of the dot. Wired via htmlFor. |
description | ReactNode | — | Optional helper text rendered under the label. |
className | string | — | Merged onto the underlying <button role="radio">. |
Accessibility
- The root renders as
<div role="radiogroup">witharia-requiredandaria-disabledreflecting the group props. - Each item renders as
<button role="radio">witharia-checked="true" | "false"anddata-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
labelis 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.requiredis reflected asaria-requiredon the radiogroup and propagated to the hidden mirror inputs whennameis set.
Credits
- Extracted from:
algoflashcards(src/platform/ui/radio-group.tsx). The source wrappedradix-ui'sRadioGroupprimitives. craft-bits drops theradix-uidependency — the group is a plain<div role="radiogroup">with its own roving-arrow logic — and broadens the API into a compoundRadioGroup.{Root,Item}plus anoptions[]shorthand with per-row label and description slots.