Radio Group
A set of mutually-exclusive options — single selection, arrow-key navigation, and disabled, built on Base UI Radio Group.
- Status
- Since
0.1.0- Accessibility pattern
- APG radio group
Last updated
Install
Add Radio Group from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/radio-groupUsage
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
<RadioGroup defaultValue="comfortable" aria-label="Density">
<RadioGroupItem value="comfortable" aria-label="Comfortable" />
<RadioGroupItem value="compact" aria-label="Compact" />
</RadioGroup>;Built on Base UI Radio Group: the group renders a
<div role="radiogroup"> and each item a styled <span role="radio"> plus a hidden <input>.
Selecting an item fires onValueChange with its value. Give every item an accessible name — wrap
it in a Field (which auto-associates the label), use
nativeButton render={<button />} when pairing a sibling <label htmlFor>, or pass an
aria-label.
Anatomy
Radio Group is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
RadioGroup is a compound component. Compose RadioGroupItems inside the root; the root owns the
selection and shares it across the items by value:
<RadioGroup defaultValue="comfortable" orientation="vertical">
<Field label="Comfortable" orientation="horizontal">
<RadioGroupItem value="comfortable" />
</Field>
<Field label="Compact" orientation="horizontal">
<RadioGroupItem value="compact" />
</Field>
</RadioGroup>RadioGroup— the root that groups the items and owns selection + layoutorientation(data-slot="radio-group",data-orientation). Controlled viavalue/onValueChange, or uncontrolled viadefaultValue. Passdisabledto disable every item at once.RadioGroupItem— an individual selectable option (data-slot="radio-group-item"). Selected state is exposed asdata-checked; the primary dot indicator scales in when checked. Each item needs a uniquevalueand an accessible name (viaField,<label>, oraria-label).
With a Field
Field with orientation="horizontal" places each radio before an inline label and wires the label
association for you — no manual htmlFor/id.
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Field } from "@/components/ui/field";
<RadioGroup defaultValue="comfortable" aria-label="Density">
<Field label="Comfortable" orientation="horizontal">
<RadioGroupItem value="comfortable" />
</Field>
<Field label="Compact" orientation="horizontal">
<RadioGroupItem value="compact" />
</Field>
</RadioGroup>;Sibling labels
Base UI's default radio root is a <span> so enclosing-label and Field patterns work well. When
your markup needs a sibling <label htmlFor>, render the item as a native button so the id targets
the visible interactive element:
<RadioGroup aria-label="Payment method">
<label htmlFor="payment-card">Card</label>
<RadioGroupItem
id="payment-card"
value="card"
nativeButton
render={<button type="button" />}
/>
</RadioGroup>States
Use value + onValueChange for controlled selection, or defaultValue for uncontrolled. A single
item can be disabled, or set disabled on the RadioGroup to disable the whole set. Every item
shows a :focus-visible ring on keyboard focus.
Orientation
Set orientation="horizontal" on the root to lay the options out in a wrapping row instead of a
column. The value is mirrored to aria-orientation; selection still moves with any arrow key. The
last option below is disabled, so the same preview covers the orientation × disabled matrix.
<RadioGroup
orientation="horizontal"
defaultValue="card"
aria-label="Payment method"
>
<RadioGroupItem value="card" aria-label="Card" />
<RadioGroupItem value="paypal" aria-label="PayPal" />
<RadioGroupItem value="bank" aria-label="Bank transfer" />
<RadioGroupItem value="wire" aria-label="Wire" disabled />
</RadioGroup>Invalid
When the group fails validation, set aria-invalid (often via a Field
error) — each item's border tints destructive (aria-invalid:border-destructive/70). Pair it with
a visible, announced error message.
Select a plan to continue.
<RadioGroup aria-label="Plan" aria-invalid>
<RadioGroupItem value="starter" aria-invalid aria-label="Starter" />
<RadioGroupItem value="pro" aria-invalid aria-label="Pro" />
</RadioGroup>Playground
Try both orientations, both item sizes, and the disabled state, then copy the generated JSX.
<RadioGroup defaultValue="comfortable">
<Field label="Comfortable" orientation="horizontal">
<RadioGroupItem value="comfortable" />
</Field>
<Field label="Compact" orientation="horizontal">
<RadioGroupItem value="compact" />
</Field>
<Field label="Spacious" orientation="horizontal">
<RadioGroupItem value="spacious" />
</Field>
</RadioGroup>API Reference
RadioGroup
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | string | — | The value selected on first render (uncontrolled). |
disabled | boolean | false | Disable the whole group — every item becomes non-interactive. |
onValueChange | ((value: string, eventDetails: BaseRadioGroup.ChangeEventDetails) => void) | — | Called with the next value whenever the selection changes. |
orientation | "horizontal" | "vertical" | — | |
value | string | — | The controlled value of the currently selected item. Pair with
onValueChange. Use defaultValue for an uncontrolled group instead. |
Data attributes and CSS variables on RadioGroup
| Attribute | Values |
|---|---|
data-orientation | mirrors a prop or state value |
data-slot | "radio-group" |
RadioGroupItem
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | The unique value this item contributes to the group when selected. |
disabled | boolean | false | Prevent the user from selecting this item while still rendering it. |
render | ComponentRenderFn<HTMLProps, RadioRootState> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Replace the rendered element via Base UI render composition. Pass a
ReactElement or a render function — Base UI merges this
item's className, data-slot, and state data-* onto your element,
forwards the ref, and keeps the <Radio.Indicator> child. The element must
support role="radio" semantics. |
size | "md" | "sm" | 'md' | Dot size — sm 14px / md 16px, mirroring Checkbox (register P1-04). |
Data attributes and CSS variables on RadioGroupItem
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "radio-group-indicator" | "radio-group-item" |
Accessibility
- Built on Base UI Radio Group: the root renders with
role="radiogroup"and each item withrole="radio", backed by a hidden<input>for form submission. - Roving tabindex — only the selected item (or the first, when none is selected) is in the tab order; arrow keys move both focus and selection between items.
- Selected items set
aria-checked="true"; the rest setaria-checked="false". - Always give each item an accessible name — wrap it in a
Field, usenativeButton render={<button />}for sibling<label htmlFor>patterns, or passaria-label. Label the group itself witharia-label/aria-labelledby. - Keyboard focus is visible — Base UI Radio Group renders its default
:focus-visiblering on the focused item (the component never setsoutline: none), and hover deepens the border (hover:border-ring/(--alpha-tint-border)).disableditems are skipped by keyboard navigation.
| Key | Action |
|---|---|
| Tab | Move focus into the group (lands on the selected item), then out. |
| ↑ / ← | Move selection to the previous item. |
| ↓ / → | Move selection to the next item. |
| Space | Select the focused item. |
| Contract | States tested |
|---|---|
| Behaviour | default, checked, disabled, dragging, error, invalid, selected |
| Accessibility | focus-visible, invalid, labeled, semantic-html |
| Visual | default, hover, disabled, invalid, checked, error, dark |