Skip to content
Component installs need the registry setup
VegaStack Design

Radio Group

A set of mutually-exclusive options — single selection, arrow-key navigation, and a 24px invisible hit area.

Status
stable
Since
0.1.0
Accessibility pattern
APG radio group

Last updated

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Radio Group before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify radio-group

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/radio-group

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/field.

Usage

import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";

<RadioGroup defaultValue="option-one">
  <div className="flex items-center gap-3">
    <RadioGroupItem value="option-one" id="option-one" />
    <Label htmlFor="option-one">Option One</Label>
  </div>
</RadioGroup>;

Anatomy

RadioGroup — data-slot="radio-group"
RadioGroupItem — data-slot="radio-group-indicator" | "radio-group-item"

Examples

Composition

RadioGroup
├── RadioGroupItem
└── RadioGroupItem

Description

Give each option helper text with FieldContent and FieldDescription.

Standard spacing for most use cases.

More space between elements.

Minimal spacing for dense layouts.

Choice Card

Wrap the whole Field in a FieldLabel for a clickable card, which tints when it is selected.

Fieldset

FieldSet and FieldLegend give the group a label and a description of its own.

Subscription Plan

Yearly and lifetime plans offer significant savings.

Disabled

disabled on an item disables that option; on the RadioGroup it disables all of them. The radio dims on its own, inside a Field or not; add data-disabled to the Field so the label dims with it.

Invalid

Use aria-invalid on the items and data-invalid on the Field. FieldError carries the message with a leading icon, so the error is never signalled by colour alone (A11Y-8).

Delivery window

RTL

The indicator dot is centred with logical translation, so a right-to-left group mirrors without any extra work.

States

Rest, selected, invalid and disabled in one frame.

Playground

Try both layouts with the disabled and invalid states, then copy the generated JSX.

<RadioGroup defaultValue="comfortable">
  <Field orientation="horizontal">
    <RadioGroupItem value="comfortable" id="comfortable" />
    <FieldLabel htmlFor="comfortable">Comfortable</FieldLabel>
  </Field>
  <Field orientation="horizontal">
    <RadioGroupItem value="compact" id="compact" />
    <FieldLabel htmlFor="compact">Compact</FieldLabel>
  </Field>
  <Field orientation="horizontal">
    <RadioGroupItem value="spacious" id="spacious" />
    <FieldLabel htmlFor="spacious">Spacious</FieldLabel>
  </Field>
</RadioGroup>

API Reference

RadioGroup adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

RadioGroupItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).

Accessibility

  • Renders Base UI's Radio Group, which gives the group role="radiogroup" and each item role="radio" with aria-checked; ↑/↓ and ←/→ move the selection, and the group is one tab stop.
  • The visible dot is 16px, so an ::after pseudo-element extends the pointer target to at least 24×24 (A11Y-2), verified by a real elementFromPoint probe.
  • On :focus-visible the global 2px outline in base.css applies (FOC-1). The recipe carries no ring of its own and no outline-none (FOC-6).
  • Bind every item to a Label or FieldLabel with htmlFor; a radio has no text of its own.
ContractStates tested
Behaviourdefault, checked, disabled, invalid
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, checked, invalid, disabled
Visualdefault, checked, invalid, disabled

Do / Don't

Do
Use a Radio Group when every option should be visible at once and exactly one applies.
Don't
Use a Radio Group for more than a handful of options — that is a Select or a Combobox.

Deviations

Upstream's file plus packages/ui/upstream/patches/radio-group.patch. Every hunk:

  • FOC-1, FOC-6 — the outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 glow is removed, along with the aria-invalid halo rings. base.css owns the single :focus-visible outline, and outline-none would suppress it.
  • FOC-14 — the invalid tint and its checked counterpart stay upstream's aria-invalid:border-destructive and aria-invalid:aria-checked:border-primary, held in every state, focused or not (FOC-14 retired FOC-5's not-focus: guard).
  • FOC-12 — upstream rings the whole choice card and cancels the control's own ring with two group-has-[:focus-visible]/field-label: rules. This system rings the control, so both are removed with the card ring they existed to serve.
  • FRM-15 — disabled:cursor-not-allowed disabled:opacity-50 become data-disabled:…. Base UI renders each item as a <span>, which :disabled never matches, so upstream's radio dimmed only inside a disabled Field; keyed on the data-disabled Base UI sets, it dims everywhere.
  • API-26 — inside a Field, the RadioGroup is the control the Field names; each item keeps its own name (Base UI would otherwise hand the Field's label to every item). An item in a Field of its own still takes that Field's label.
  • DOC-2 — cn is imported from @vegastack/design.
  • A11Y-24 — an item's invisible hit area grows to 44×44 on a coarse pointer. Widened by MK, 2026-10-26.

On this page