Skip to content

ComponentsForms

Radio Group

Lets users pick exactly one option from a small set that stays visible. Renders as a simple list or as selectable cards with icons, and supports descriptions, horizontal layout, validation and native form submission.

React AriaSource
Notifications

Installation

pnpm dlx shadcn@latest add @desyne/radio-group

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import { Radio, RadioCard, RadioGroup } from "@/components/ui/radio-group";
tsx
<RadioGroup label="Size" defaultValue="m">
  <Radio value="s">Small</Radio>
  <Radio value="m">Medium</Radio>
  <Radio value="l">Large</Radio>
</RadioGroup>

The group owns the value: a single string matching one radio's value. Radio and RadioCard can be mixed freely inside the same group.

When to use

  • Radio Group — one choice from about 2–6 options that users should compare side by side.
  • Select — one choice from a longer list, or when space is tight.
  • Checkbox — any number of choices, or a single yes/no that's submitted with a form.
  • Switch — a single on/off setting that applies immediately.
  • Toggle Button Group — a compact, button-styled choice such as alignment or view mode.

Anatomy

tsx
<RadioGroup>                {/* label, description, error message */}
  <Radio value="…">Label</Radio>
  <RadioCard value="…" icon={…} title="…" description="…" />
</RadioGroup>
PartRendersNotes
RadioGroup<div role="radiogroup">Wraps the label, description, radios and error message. orientation controls the layout.
Radio<label> + hidden <input type="radio">A dot and a label. With description, the label becomes a bold title with secondary text under it.
RadioCard<label> + hidden <input type="radio">A bordered card with an optional icon, a title, a description and a trailing dot.
Label / Description<span>Rendered from the group's label and description props.
FieldError<div>Rendered by the group when validation fails; shows errorMessage or the browser message.

Examples

Horizontal

orientation="horizontal" lays radios out in a wrapping row and sets aria-orientation. Use it for 2–4 short labels.

Density

With descriptions

description on a Radio adds secondary text under the label. Use it when the labels alone don't explain the difference between options.

Visibility

Disabled and read-only

isDisabled on a Radio makes just that option unavailable while keeping it visible; explain why in the group's description. isDisabled on the group disables every option. isReadOnly shows the value but ignores changes, and keeps the group focusable.

RegionTokyo is at capacity. Try again next week.
Billing cycle
Data residency

Controlled

Use value and onChange to own the selection. onChange receives the selected radio's value.

Digest frequency

You'll get a digest weekly.

Cards

RadioCard is a larger target with a leading icon, a title and a description. The whole card is the label, and the selected card gets a brand-colored border.

Plan

Card grid

In a horizontal group, give each card flex-1 and a min-w-* so they share the row evenly and wrap on narrow screens.

Payment method

With a follow-up input

Control the group to reveal a related field when a specific option is picked, like "Something else". Place the input after the group, not inside a radio's label.

Why are you cancelling?

Required and validation

isRequired blocks form submission until an option is selected and adds a * to the label. With native validation, the errorMessage appears after the first submit attempt.

Team size

Custom validation

validate receives the selected value and returns an error string, or null when valid. With validationBehavior="aria" the message updates live as the selection changes.

Shipping to a P.O. box

Form submission

Give the group a name and the selected radio's value is submitted under it, exactly like native radio buttons.

Size
Milk

Recipes

Plan picker

Cards whose title is a composed node with a Badge and a price. A Switch toggles yearly billing.

Plan

Shipping method

A checkout step where the selected card updates the order total.

Shipping method
Subtotal
$128.00
Shipping
Free
Total
$128.00

Privacy settings

Two controlled groups in a settings card, with Save and Discard enabled only when the draft differs from the saved value.

Privacy

Control who can find you and contact you.

Who can see your profile
Who can message you

Accessibility

  • Every radio is a native <input type="radio"> visually hidden inside a <label>, so the whole label (or card) is clickable and native forms work.
  • The group has role="radiogroup", is labelled by label, and links its description and error message with aria-describedby.
  • The group is a single tab stop: focus lands on the selected radio (or the first one when nothing is selected), and arrow keys move the selection.
  • A group without a visible label needs aria-label or aria-labelledby.
  • description on a Radio and title / description on a RadioCard are inside the <label>, so they're read as part of the option's name. Keep them concise.
  • The focus ring only appears for keyboard focus (data-focus-visible).

Keyboard

KeyAction
TabMoves focus into the group, to the selected radio
↓ / →Selects the next radio, wrapping to the first
↑ / ←Selects the previous radio, wrapping to the last
SpaceSelects the focused radio, if it isn't already

Styling

Data attributes

On Radio and RadioCard (target inner parts with group-data-*/radio:):

AttributePresent when
data-selectedThe radio is the group's value
data-hoveredHovered with a mouse or pen
data-pressedBeing pressed
data-focused / data-focus-visibleFocused / focused with the keyboard
data-disabledDisabled, directly or through the group
data-readonlyThe group is read-only
data-invalidThe group is invalid
data-requiredThe group is required

On RadioGroup (children can use group-data-*/field:):

AttributePresent when
data-orientation"horizontal" or "vertical"
data-disabled / data-readonlyDisabled / read-only
data-required / data-invalidRequired / validation failed

Slots

data-slotElement
radio-groupGroup root
radioEach Radio's <label> root
radio-cardEach RadioCard's <label> root
label, description, field-errorGroup label, description and error message

Color

The dot, hover border and selected card use the --brand theme variable. Override it on a group or a single radio:

tsx
<RadioGroup label="Status" className="[--brand:var(--color-emerald-600)]">
  …
</RadioGroup>

Render props

Radio's className and children accept a function of its state; RadioCard's className does too:

tsx
<Radio value="pro" className={({ isSelected }) => (isSelected ? "font-medium" : "")}>
  Pro
</Radio>

API Reference

RadioGroup

Prop

Type

Also accepts every prop of React Aria's RadioGroup.

Radio

Prop

Type

RadioCard

Prop

Type

RadioCard accepts the same React Aria props as Radio, except children: its content comes from title, description and icon. See React Aria's Radio props.