Skip to main content
Gremorie

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>

PropTypeDefaultDescription
valuestring-Controlled selected value.
defaultValuestring-Uncontrolled initial value.
onValueChange(value: string) => void-Fires when the user selects a different option.
disabledbooleanfalseDisables the whole group.
requiredbooleanfalseMarks the group as required for form submission.
namestring-Form field name.
orientation"horizontal" | "vertical""vertical"Affects arrow-key navigation direction.
loopbooleantrueWhen 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>

PropTypeDefaultDescription
valuestring-The value reported back to the parent. Required.
disabledbooleanfalseDisables this single option.

Renders a circular target with the CircleIcon indicator visible only when selected. Carries data-slot="radio-group-item".

Composition

  1. <RadioGroup> is the root context. It owns the selected value and orientation.
  2. Each <RadioGroupItem> is paired with a <Label> via htmlFor matching the item's id. The label is the affordance most users click.
  3. 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 carries role="radio" with aria-checked reflecting state.
  • Roving tabindex: only the selected (or first, if none selected) item is in the tab order. Tab moves out of the group; arrow keys move within.
  • Keyboard:
    • ArrowDown / ArrowRight moves to the next option (and selects it).
    • ArrowUp / ArrowLeft moves to the previous option.
    • Home / End jump to first / last.
    • Space selects 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.
  • 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

On this page