Radio Group
Single-select group of mutually exclusive options built on Radix RadioGroup, with roving tabindex and arrow-key navigation.
Overview
RadioGroup renders a set of mutually exclusive options. Built on @radix-ui/react-radio-group, the root owns the selected value and each RadioGroupItem represents one choice. Cap visible options at five - beyond that, prefer Select for vertical-space efficiency.
The Radix primitive handles the roving tabindex pattern (only the selected option is in the tab order) and arrow-key navigation automatically. You don't need to wire onKeyDown or tabIndex manually.
Preview
Installation
bash npx gremorie@latest add rx-radio-group bash pnpm dlx gremorie@latest add rx-radio-group
bash yarn dlx gremorie@latest add rx-radio-group
bash bunx --bun gremorie@latest add rx-radio-group
Usage
import { Label, RadioGroup, RadioGroupItem } from "@gremorie/rx-forms";
export function Example() {
return (
<RadioGroup defaultValue="react">
<div className="flex items-center gap-2">
<RadioGroupItem id="rg-react" value="react" />
<Label htmlFor="rg-react">React</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem id="rg-ng" value="angular" />
<Label htmlFor="rg-ng">Angular</Label>
</div>
</RadioGroup>
);
}Angular edition planned for Phase 5h. Star the repo to track progress.
API
<RadioGroup>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled selected value. |
defaultValue | string | - | Uncontrolled initial value. |
onValueChange | (value: string) => void | - | Fires when the user selects a different option. |
disabled | boolean | false | Disables the whole group. |
required | boolean | false | Marks the group as required for form submission. |
name | string | - | Form field name. |
orientation | "horizontal" | "vertical" | "vertical" | Affects arrow-key navigation direction. |
loop | boolean | true | When true, arrow keys wrap from last to first. |
Forwards to RadioGroupPrimitive.Root. Renders as a CSS Grid with gap-3 by default; override className for custom layout.
<RadioGroupItem>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The value reported back to the parent. Required. |
disabled | boolean | false | Disables this single option. |
Renders a circular target with the CircleIcon indicator visible only when selected. Carries data-slot="radio-group-item".
Composition
<RadioGroup>is the root context. It owns the selected value and orientation.- Each
<RadioGroupItem>is paired with a<Label>viahtmlFormatching the item'sid. The label is the affordance most users click. - For form integration, wrap with
<FormField>+<FormControl>so ARIA wiring and validation propagate.
Variations
Vertical list (default)
The canonical pattern for 2-5 options. Vertical layout makes scanning easy.
<RadioGroup defaultValue="monthly">
<div className="flex items-center gap-2">
<RadioGroupItem id="monthly" value="monthly" />
<Label htmlFor="monthly">Monthly billing</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem id="yearly" value="yearly" />
<Label htmlFor="yearly">Yearly billing</Label>
</div>
</RadioGroup>Horizontal for short labels
When options are short and the surrounding context is wide enough, switch to horizontal orientation.
<RadioGroup
defaultValue="left"
orientation="horizontal"
className="flex flex-row gap-4"
>
<div className="flex items-center gap-2">
<RadioGroupItem id="left" value="left" />
<Label htmlFor="left">Left</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem id="center" value="center" />
<Label htmlFor="center">Center</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem id="right" value="right" />
<Label htmlFor="right">Right</Label>
</div>
</RadioGroup>Controlled with description
Add a hint paragraph after each label when the choice has consequences worth explaining.
function PlanPicker() {
const [plan, setPlan] = React.useState('pro');
return (
<RadioGroup value={plan} onValueChange={setPlan}>
<div className="flex items-start gap-2">
<RadioGroupItem id="free" value="free" className="mt-0.5" />
<div className="grid gap-1">
<Label htmlFor="free">Free</Label>
<p className="text-sm text-muted-foreground">
3 projects, community support.
</p>
</div>
</div>
<div className="flex items-start gap-2">
<RadioGroupItem id="pro" value="pro" className="mt-0.5" />
<div className="grid gap-1">
<Label htmlFor="pro">Pro</Label>
<p className="text-sm text-muted-foreground">
Unlimited projects, priority support.
</p>
</div>
</div>
</RadioGroup>
);
}Accessibility
- ARIA radiogroup pattern: root carries
role="radiogroup"; each item carriesrole="radio"witharia-checkedreflecting state. - Roving tabindex: only the selected (or first, if none selected) item is in the tab order.
Tabmoves out of the group; arrow keys move within. - Keyboard:
ArrowDown/ArrowRightmoves to the next option (and selects it).ArrowUp/ArrowLeftmoves to the previous option.Home/Endjump to first / last.Spaceselects the focused option when none is selected.
- Loop: arrow keys wrap by default. Pass
loop={false}if your design expects boundary behaviour. - Disabled items are skipped during keyboard navigation.
Related
- Checkbox - multi-select sibling
- Select - dropdown alternative for longer lists
- Toggle Group - icon-led single-select for compact toolbars
- Label - the canonical companion
- Form - wire RadioGroup into react-hook-form