Radio Group
A set of mutually-exclusive options — single selection, arrow-key navigation, and a 24px invisible hit area.
- Status
- 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-groupInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/radio-groupRun 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
Examples
Composition
RadioGroup
├── RadioGroupItem
└── RadioGroupItemDescription
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.
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).
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 itemrole="radio"witharia-checked; ↑/↓ and ←/→ move the selection, and the group is one tab stop. - The visible dot is 16px, so an
::afterpseudo-element extends the pointer target to at least 24×24 (A11Y-2), verified by a realelementFromPointprobe. - On
:focus-visiblethe global 2px outline inbase.cssapplies (FOC-1). The recipe carries no ring of its own and nooutline-none(FOC-6). - Bind every item to a
LabelorFieldLabelwithhtmlFor; a radio has no text of its own.
| Contract | States tested |
|---|---|
| Behaviour | default, checked, disabled, invalid |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, checked, invalid, disabled |
| Visual | default, checked, invalid, disabled |
Do / Don't
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/50glow is removed, along with thearia-invalidhalo rings.base.cssowns the single:focus-visibleoutline, andoutline-nonewould suppress it. - FOC-14 — the invalid tint and its checked counterpart stay upstream's
aria-invalid:border-destructiveandaria-invalid:aria-checked:border-primary, held in every state, focused or not (FOC-14 retired FOC-5'snot-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-50becomedata-disabled:…. Base UI renders each item as a<span>, which:disablednever matches, so upstream's radio dimmed only inside a disabledField; keyed on thedata-disabledBase UI sets, it dims everywhere. - API-26 — inside a
Field, theRadioGroupis 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 aFieldof its own still takes that Field's label. - DOC-2 —
cnis 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.