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

'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 selected

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>
  );
}

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>

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>

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 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