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
'use client';import { Label, RadioGroup, RadioGroupItem } from '@gremorie/rx-forms';export function RadioGroupPreview() { return ( <RadioGroup defaultValue="react" className="flex flex-col gap-2"> <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> <div className="flex items-center gap-2"> <RadioGroupItem id="rg-both" value="both" /> <Label htmlFor="rg-both">Both</Label> </div> </RadioGroup> );}Anatomy
RadioGroup the Root, a grid gap-3 owning the value
└─ RadioGroupItem one circular option (value required); pair with a Label
└─ Indicator filled CircleIcon dot, shown only when selectedInstallation
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>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
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>With descriptions
Add a hint paragraph after each label when the choice has consequences worth explaining. Align the item to the top of the row with mt-0.5.
Balanced spacing for most layouts.
Roomier rows for dense data tables.
Tighter rows to fit more on screen.
'use client';import { Label, RadioGroup, RadioGroupItem } from '@gremorie/rx-forms';export function RadioGroupWithDescriptionPreview() { return ( <RadioGroup defaultValue="comfortable"> <div className="flex items-start gap-3"> <RadioGroupItem value="default" id="rg-default" className="mt-0.5" /> <div className="grid gap-1"> <Label htmlFor="rg-default">Default</Label> <p className="text-sm text-muted-foreground"> Balanced spacing for most layouts. </p> </div> </div> <div className="flex items-start gap-3"> <RadioGroupItem value="comfortable" id="rg-comfortable" className="mt-0.5" /> <div className="grid gap-1"> <Label htmlFor="rg-comfortable">Comfortable</Label> <p className="text-sm text-muted-foreground"> Roomier rows for dense data tables. </p> </div> </div> <div className="flex items-start gap-3"> <RadioGroupItem value="compact" id="rg-compact" className="mt-0.5" /> <div className="grid gap-1"> <Label htmlFor="rg-compact">Compact</Label> <p className="text-sm text-muted-foreground"> Tighter rows to fit more on screen. </p> </div> </div> </RadioGroup> );}Disabled option
Disable a single RadioGroupItem to lock out an unavailable choice while keeping the rest of the group interactive. Disabled items are skipped during keyboard navigation.
'use client';import { Label, RadioGroup, RadioGroupItem } from '@gremorie/rx-forms';export function RadioGroupDisabledPreview() { return ( <RadioGroup defaultValue="standard"> <div className="flex items-center gap-3"> <RadioGroupItem value="standard" id="rg-standard" /> <Label htmlFor="rg-standard">Standard shipping</Label> </div> <div className="flex items-center gap-3"> <RadioGroupItem value="express" id="rg-express" disabled /> <Label htmlFor="rg-express">Express (currently unavailable)</Label> </div> <div className="flex items-center gap-3"> <RadioGroupItem value="overnight" id="rg-overnight" /> <Label htmlFor="rg-overnight">Overnight</Label> </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