Radio Group
A single choice from a small set of visible options. It owns its label and its error message like the rest of the field family. One size, no variants — error and disabled are states.
className styles the wrapper
RadioGroup renders a wrapper around the option list so it can hold the group label and the error message, the same as Checkbox and Select. className styles that wrapper, not the list. On RadioGroupItem it styles the row that holds the circle and its label. Every other prop passes through to the matching Radix part.
States
One 20px circle, matching Checkbox's box, on a 20px row. The checked item fills --indicator and knocks an 8px dot out of it, so a radio sitting beside a checkbox in the same form reads as the same family — not the unfilled circle with a colored dot that most libraries draw. Hover steps an unchecked border to --indicator and a checked disc a shade lighter. There is no size prop and no loading state: an option states intent inside a form, and the submit button owns the busyness.
Basic
RadioGroup takes label, error, value, defaultValue, onValueChange, name, disabled, and required. RadioGroupItem takes value, label, and disabled — and nothing else. There is no description line, no ReactNode label, and no card-style item: an option that needs a price or a badge beside it is a group the app composes from Radix itself.
Horizontal
orientation takes RadioGroupOrientation.Horizontal and lays the options out in a row. Radix remaps the arrow keys to match. Horizontal is for short labels. A row of full sentences wraps unpredictably and loses the alignment that makes a set of options scannable.
Error and disabled
Pass error and the group label, the item borders, the focus ring, and the checked disc all turn destructive, with the message below. The disc keeps its knocked-out dot; only the fill changes color. disabled on the group dims and disables every option; disabled on one item takes that option out while the rest stay live.
Overnight is the only speed available today
Overnight is the only speed available today
Controlled
value plus onValueChange hands the selection to the app; defaultValue leaves it with the component. name puts the chosen value into the surrounding form's FormData, so a plain native submit works with no JavaScript of your own. These are the props a form-state library drives — the registry binds to none of them itself.
Paying by Card.
Paying by Card.
Motion
The dot is mounted permanently and fades between opacity 0 and 1 on a CSS transition-opacity at --motion-base. It does not scale: a spring describes movement, and a lone opacity value has none to describe. The disc fill, the border, and the focus ring are CSS transitions at --motion-fast underneath, the same headline-over-feedback split Checkbox runs. Radio Group is the first field-family member with no motion dependency — the animated error message below it still comes from the field item.
Accessibility
The group exposes role="radiogroup" named by its label through aria-labelledby — no fieldset and no legend, because the role already carries the grouping. Each item generates its own id and wires its label htmlFor, so clicking a label selects that option. Tab enters the group once, landing on the checked option or the first enabled one, and the arrow keys move the selection from there; Space selects the focused option. An error sets aria-invalid on the group and links the message through aria-describedby, keeping any description you passed. The focus ring is keyboard-only and sits 2px clear of the circle, the same 3px ring Button, Checkbox, and Switch use. required marks the label and sets aria-required on the group; the validation itself stays with your app.