Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Radio Group

A set of mutually-exclusive options — single selection, arrow-key navigation, and disabled, built on Base UI Radio Group.

Status
stable
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-group

Usage

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

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

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 + layout orientation (data-slot="radio-group", data-orientation). Controlled via value/onValueChange, or uncontrolled via defaultValue. Pass disabled to disable every item at once.
  • RadioGroupItem — an individual selectable option (data-slot="radio-group-item"). Selected state is exposed as data-checked; the primary dot indicator scales in when checked. Each item needs a unique value and an accessible name (via Field, <label>, or aria-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.

<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

PropTypeDefaultDescription
defaultValuestringThe value selected on first render (uncontrolled).
disabledbooleanfalseDisable 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"
valuestringThe controlled value of the currently selected item. Pair with onValueChange. Use defaultValue for an uncontrolled group instead.

Data attributes and CSS variables on RadioGroup

AttributeValues
data-orientationmirrors a prop or state value
data-slot"radio-group"

RadioGroupItem

PropTypeDefaultDescription
value*stringThe unique value this item contributes to the group when selected.
disabledbooleanfalsePrevent the user from selecting this item while still rendering it.
renderComponentRenderFn<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

AttributeValues
data-sizemirrors 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 with role="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 set aria-checked="false".
  • Always give each item an accessible name — wrap it in a Field, use nativeButton render={<button />} for sibling <label htmlFor> patterns, or pass aria-label. Label the group itself with aria-label/aria-labelledby.
  • Keyboard focus is visible — Base UI Radio Group renders its default :focus-visible ring on the focused item (the component never sets outline: none), and hover deepens the border (hover:border-ring/(--alpha-tint-border)). disabled items are skipped by keyboard navigation.
KeyAction
TabMove focus into the group (lands on the selected item), then out.
/ Move selection to the previous item.
/ Move selection to the next item.
SpaceSelect the focused item.
ContractStates tested
Behaviourdefault, checked, disabled, dragging, error, invalid, selected
Accessibilityfocus-visible, invalid, labeled, semantic-html
Visualdefault, hover, disabled, invalid, checked, error, dark

Do / Don't

Do
Give every item a label and a unique value, and label the group itself; use it for 2–5 mutually-exclusive choices.
Don't
Use htmlFor sibling labels against the default span root, a radio group for many options (use a Select), or radios for non-exclusive choices.

On this page