Components

Radio Group

A set of mutually exclusive options where exactly one item can be selected at a time. Built on Radix UI with full keyboard navigation and roving focus.

Basic usage

Pair each RadioGroupItem with a Label and set defaultValue for an uncontrolled group. The root is a grid gap-3, so stacked options need no extra layout classes.

Card selection

Wrap each item in a Label and match on has-[[data-state=checked]] to highlight the selected card. The item is a button, not a native input, so has-[:checked] would only ever match inside a form — Radix's data-state always matches. Use the brand role tokens for the highlight: border-primary-ink for the border and a bg-primary/10 tint for the fill.

Horizontal

orientation tells the roving-focus manager which arrow keys move between items — it does not change the layout. Add grid-flow-col (or your own flex classes) to place the options in a row.

Invalid

Set aria-invalid on the items after a failed validation. The border switches to border-destructive-ink — the ink step, which stays readable on the high-contrast palette where --destructive is a fill colour rather than a line colour.

Shipping speed

Pick a shipping speed to continue.

Disabled items

Add disabled to a single RadioGroupItem to skip it during keyboard navigation, or to the group to disable every option.

Props

RadioGroup

PropTypeDefaultDescription
defaultValuestringThe value of the item checked by default in an uncontrolled group.
valuestring | nullThe controlled value of the checked item. Pair with onValueChange.
onValueChange(value: string) => voidCallback fired when the selected item changes.
orientation'horizontal' | 'vertical'Arrow-key direction for roving focus; both axes work when it is left unset. It does not lay the group out — the root is a grid, so pass className to change the visual direction.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
dir'ltr' | 'rtl'Reading direction used for horizontal arrow navigation.
disabledbooleanfalseDisables every item in the group.
requiredbooleanfalseMarks selection as required when used inside a form.
namestringThe name submitted with the group in a native form.
classNamestringMerged onto the root, which defaults to grid gap-3.
...restRadix RadioGroup.Root propsEverything else is forwarded to the root div.

RadioGroupItem

PropTypeDefaultDescription
valuerequiredstringThe unique value this item represents.
disabledbooleanfalseDisables this individual item.
requiredbooleanfalseMarks this item as required for native form validation.
idstringElement id, used to associate a Label via htmlFor.
aria-invalidbooleanSwitches the border to border-destructive-ink and tints the ring — use it for failed validation.
classNamestringAdditional Tailwind classes to merge.
...restRadix RadioGroup.Item propsEverything else is forwarded to the underlying button.

The package also exports the prop types RadioGroupProps and RadioGroupItemProps for wrappers built on top of these.

Accessibility

  • Implements the WAI-ARIA radio group pattern with role="radiogroup" and roving tabindex — the group is a single Tab stop.
  • Arrow keys move between options and select them; Space selects the focused item, and disabled items are skipped.
  • Associate each item with a Label via matching id / htmlFor so the label is announced and becomes a click target.
  • A focus-visible ring using the brand gold ring token marks the focused item during keyboard navigation.
  • The item has a transparent fill, so the selected dot is the only thing that carries the state — a non-text visual under WCAG 1.4.11. It is painted with fill-control-on, not fill-primary: the raw brand gold scores 1.86:1 on a white page, while the high-contrast palette re-tunes --control-on to #B48700 in light (3.06–3.28:1 across the surfaces a radio can sit on) and to the light gold step in dark (10.80–12.67:1).
  • In dark mode the ring alone would be almost invisible against the card, so the item adds a dark:bg-input-fill wash. That variable collapses to transparent on the high-contrast palette, where the border is already strong enough on its own.