Radio Group
Lets people choose exactly one option from a small set, with every option visible at once.
import { Field, FieldLabel } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
const frequencies = [
{ value: "realtime", label: "As it happens" },
{ value: "daily", label: "Daily summary" },
{ value: "weekly", label: "Weekly summary" },
]
export default function RadioGroupDemo() {
return (
<RadioGroup
defaultValue="daily"
aria-label="Notification frequency"
className="w-52"
>
{frequencies.map((frequency) => (
<Field key={frequency.value} orientation="horizontal">
<RadioGroupItem
value={frequency.value}
id={`frequency-${frequency.value}`}
/>
<FieldLabel
htmlFor={`frequency-${frequency.value}`}
className="font-normal"
>
{frequency.label}
</FieldLabel>
</Field>
))}
</RadioGroup>
)
}Anatomy
- 1ControlThe circle. Its border stands 3:1 from the page, and an invisible 24px target surrounds it.
- 2LabelNames the option. A click on it selects the option.
- 3IndicatorThe dot of the selected option. It grows in on a spring.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/radio-group.jsonUsage
import { Field, FieldLabel } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"<RadioGroup defaultValue="daily">
<Field orientation="horizontal">
<RadioGroupItem value="daily" id="daily" />
<FieldLabel htmlFor="daily">Daily summary</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="weekly" id="weekly" />
<FieldLabel htmlFor="weekly">Weekly summary</FieldLabel>
</Field>
</RadioGroup>The selected radio fills with the brand color and its dot springs into place.
Examples
With descriptions
When options need explaining, pair each label with a description in FieldContent. People compare options by reading the descriptions side by side — one of the main reasons to prefer radios over a select.
Only invited members can see and join this project.
Anyone in your organization can find and view it.
Anyone on the internet can view it. Only members can edit.
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
const visibility = [
{
value: "private",
label: "Private",
description: "Only invited members can see and join this project.",
},
{
value: "internal",
label: "Internal",
description: "Anyone in your organization can find and view it.",
},
{
value: "public",
label: "Public",
description: "Anyone on the internet can view it. Only members can edit.",
},
]
export default function RadioGroupDescriptions() {
return (
<RadioGroup
defaultValue="internal"
aria-label="Project visibility"
className="max-w-sm gap-4"
>
{visibility.map((option) => (
<Field key={option.value} orientation="horizontal">
<RadioGroupItem
value={option.value}
id={`visibility-${option.value}`}
/>
<FieldContent>
<FieldLabel htmlFor={`visibility-${option.value}`}>
{option.label}
</FieldLabel>
<FieldDescription>{option.description}</FieldDescription>
</FieldContent>
</Field>
))}
</RadioGroup>
)
}Choice cards
Wrap each field in a FieldLabel to make the whole card clickable. Cards suit decisions with a price, a limit or a feature list — plans, shipping speeds, deployment targets.
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
FieldTitle,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
const plans = [
{
value: "hobby",
title: "Hobby",
description: "1 project · Community support",
price: "$0",
},
{
value: "pro",
title: "Pro",
description: "Unlimited projects · Email support",
price: "$20",
},
{
value: "team",
title: "Team",
description: "Roles, SSO and audit logs",
price: "$60",
},
]
export default function RadioGroupChoiceCards() {
return (
<RadioGroup
defaultValue="pro"
aria-label="Plan"
className="w-full max-w-md"
>
{plans.map((plan) => (
<FieldLabel key={plan.value} htmlFor={`plan-${plan.value}`}>
<Field orientation="horizontal">
<RadioGroupItem value={plan.value} id={`plan-${plan.value}`} />
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<span className="text-sm font-semibold text-foreground tabular-nums">
{plan.price}
<span className="font-normal text-muted-foreground">/mo</span>
</span>
</Field>
</FieldLabel>
))}
</RadioGroup>
)
}Horizontal
Lay short options out in a row when they read as a scale or a set of peers. Use FieldSet and FieldLegend to name the group; a row of radios without a question is ambiguous.
import { Field, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
const densities = [
{ value: "compact", label: "Compact" },
{ value: "default", label: "Default" },
{ value: "comfortable", label: "Comfortable" },
]
export default function RadioGroupHorizontal() {
return (
<FieldSet className="w-fit">
<FieldLegend variant="label">Table density</FieldLegend>
<RadioGroup
defaultValue="default"
className="flex flex-wrap gap-x-6 gap-y-3"
>
{densities.map((density) => (
<Field
key={density.value}
orientation="horizontal"
className="w-auto"
>
<RadioGroupItem
value={density.value}
id={`density-${density.value}`}
/>
<FieldLabel
htmlFor={`density-${density.value}`}
className="font-normal"
>
{density.label}
</FieldLabel>
</Field>
))}
</RadioGroup>
</FieldSet>
)
}States
Disable a group when the choice doesn't apply yet, and explain why nearby. For validation, set required, mark the items aria-invalid and show a FieldError under the group.
import {
Field,
FieldError,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
export default function RadioGroupStates() {
return (
<div className="grid w-full max-w-lg gap-10 sm:grid-cols-2">
<FieldSet>
<FieldLegend variant="label">Billing cycle</FieldLegend>
<RadioGroup defaultValue="monthly" disabled>
<Field orientation="horizontal" data-disabled>
<RadioGroupItem value="monthly" id="cycle-monthly" />
<FieldLabel htmlFor="cycle-monthly" className="font-normal">
Monthly
</FieldLabel>
</Field>
<Field orientation="horizontal" data-disabled>
<RadioGroupItem value="yearly" id="cycle-yearly" />
<FieldLabel htmlFor="cycle-yearly" className="font-normal">
Yearly
</FieldLabel>
</Field>
</RadioGroup>
</FieldSet>
<FieldSet data-invalid>
<FieldLegend variant="label">Shipping speed</FieldLegend>
<RadioGroup aria-invalid required>
<Field orientation="horizontal">
<RadioGroupItem value="standard" id="ship-standard" aria-invalid />
<FieldLabel htmlFor="ship-standard" className="font-normal">
Standard (3–5 days)
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="express" id="ship-express" aria-invalid />
<FieldLabel htmlFor="ship-express" className="font-normal">
Express (1–2 days)
</FieldLabel>
</Field>
</RadioGroup>
<FieldError>Choose a shipping speed.</FieldError>
</FieldSet>
</div>
)
}Guidelines
When to use
- To choose exactly one option from two to about seven.
- When people benefit from seeing and comparing every option at once.
When not to use
- To select several options — use Checkboxes.
- For a single on/off setting — use a Switch or a checkbox.
- For more than about seven options, or when space is tight — use a Select.
- For compact segmented choices in toolbars (view modes, alignment) — use a Toggle Group.
Defaults
Preselect the most common or safest option — a radio group can't be returned to "nothing selected" once people choose, so an empty group only makes sense when the choice must be deliberate. In that case, validate it.
Standard — free
Express — $12
Subscribe to the newsletter
Order
Order options logically — by magnitude, time, or likelihood — and keep the list vertical unless the options are very short. Align radios on the left so the labels form a clean, scannable column.
Accessibility
RadioGroup renders role="radiogroup" and each item role="radio" with aria-checked. The group behaves as a single tab stop with roving focus, following the ARIA radio group pattern.
| Key | Behavior |
|---|---|
Tab | Moves focus into the group — to the checked radio, or the first one if none is checked — and out of it. |
↓→ | Moves to and checks the next radio, wrapping at the end. |
↑← | Moves to and checks the previous radio, wrapping at the start. |
Space | Checks the focused radio if it isn't checked yet. |
- Name the group. Use
FieldSet+FieldLegend, oraria-label/aria-labelledbyonRadioGroup, so the question is announced along with each option. - Label every radio with a
FieldLabelthat points at itsid. Base UI links the label to the visible radio, so clicking the text selects it. - Boundaries and targets. Radios use the
inputtoken for their border (3:1, WCAG 1.4.11) and a 24×24px hit area around the 16px circle.
API reference
RadioGroup
Renders a <div role="radiogroup">. Accepts every prop of Base UI's RadioGroup.
RadioGroupItem
Renders a <span role="radio"> with a hidden input. Accepts every prop of Base UI's Radio.Root.