Radio group
Choose exactly one option from a small set. Same visual patterns as Checkbox but circular and mutually exclusive. The RadioField compound handles label + description in a single component. Also supports a 'Box' variant where each option is a selectable card.
Anatomy
Up to 3 seats, basic reporting.
Up to 20 seats, advanced reporting.
- 1RadioGroup (root)
Radix root that manages the selected value. Uncontrolled by default via defaultValue.
- 2RadioGroupItem
The circular radio (size-4, border-input, rounded-full). Checked state shows a filled inner dot (Circle weight='fill', size-2, text-primary).
- 3Label
The clickable text next to the radio. Uses htmlFor to bind to the radio's id (RadioField wires it for you).
- 4Description
Optional muted text-xs line below the label. Adds context when options aren't self-explanatory.
- 5Box wrapper (optional)
For the Box variant: a <label> wrapping the radio + text with border rounded-lg p-4. Selected state adds border-primary + bg-primary/10.
Primitive
The base RadioGroup + RadioGroupItem give you full layout control. Use them when a table row or custom layout requires it.
<RadioGroup defaultValue="usd">
<div className="flex items-center gap-3">
<RadioGroupItem value="usd" id="usd" />
<Label htmlFor="usd">USD</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="cop" id="cop" />
<Label htmlFor="cop">COP</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="mxn" id="mxn" />
<Label htmlFor="mxn">MXN</Label>
</div>
</RadioGroup><RadioGroup defaultValue="on">
<RadioGroupItem value="on" />
<RadioGroupItem value="off" />
<RadioGroupItem value="disabled" disabled />
<RadioGroupItem value="disabled-on" disabled />
</RadioGroup>RadioField
Compound that pairs each RadioGroupItem with its label and optional description. Same API as CheckboxField.
<RadioGroup defaultValue="monthly">
<RadioField value="monthly" label="Monthly" />
<RadioField value="annual" label="Annual" />
<RadioField value="lifetime" label="Lifetime" />
</RadioGroup>Up to 3 seats, basic reporting.
Up to 20 seats, advanced reporting and rules.
Custom seats, SSO, dedicated support.
<RadioGroup defaultValue="team">
<RadioField
value="starter"
label="Starter"
description="Up to 3 seats, basic reporting."
/>
<RadioField
value="team"
label="Team"
description="Up to 20 seats, advanced reporting and rules."
/>
<RadioField
value="enterprise"
label="Enterprise"
description="Custom seats, SSO, dedicated support."
/>
</RadioGroup>Box variant
Each option is a selectable card: radio + label + description inside a rounded-lg border container. Selected state uses border-primary + bg-primary/10. Ideal for plan pickers, payment method pickers, and any choice where the surface itself should read as clickable.
{/* Recipe: wrap each option in a <label> with border rounded-lg p-4.
Selected state uses has-[[data-state=checked]]:border-primary + has-[[data-state=checked]]:bg-primary/10. */}
<RadioGroup defaultValue="team">
<label className="group flex cursor-pointer items-start gap-3 rounded-lg border border-input bg-background p-4
hover:border-ring/60
has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/10
has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring/50 has-[:focus-visible]:ring-offset-1">
<RadioGroupItem value="starter" className="mt-0.5" />
<div className="grid gap-0.5">
<span className="text-sm font-medium text-foreground">Starter</span>
<span className="text-xs text-muted-foreground">Up to 3 seats, basic reporting.</span>
</div>
</label>
{/* …repeat for other options */}
</RadioGroup>Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
Stack of Box options one below the other with a gap between them. Each card is independent — its own border and rounded corners on all sides. Ideal when options are read top-to-bottom (plan picker, feature selection).
Row / grid of Box options side by side with a gap between them. Each card is independent. Use for short-label pickers (payment method, tier).
Stack of Box options with shared borders — no gap between them. Overlapping borders collapse to 1px via -mt-px on all but the first. First card rounded-t-lg, last rounded-b-lg, middles no rounded. Active card z-10 + border-primary + bg-primary/10 draws over neighbors.
{/* Joined vertical: no gap between cards, overlapping borders collapse via -mt-px.
First: rounded-t-lg. Last: rounded-b-lg. Middle: no rounded corners.
Active card: z-10 + border-primary + bg-primary/10 (draws over neighbors). */}
<RadioGroup defaultValue="option-2" className="grid">
{items.map((o, i, arr) => {
const isFirst = i === 0;
const isLast = i === arr.length - 1;
return (
<label key={o.key} htmlFor={o.key}
className={cn(
"relative flex cursor-pointer items-start gap-3 border border-input bg-background p-3 transition-colors",
!isFirst && "-mt-px",
isFirst && "rounded-t-lg",
isLast && "rounded-b-lg",
"hover:z-10 hover:border-foreground/20",
"has-[[data-state=checked]]:z-10 has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/10",
)}
>
<RadioGroupItem value={o.key} id={o.key} className="mt-0.5" />
<div className="grid gap-1.5">
<span className="text-sm font-medium leading-none text-foreground">{o.label}</span>
<span className="text-sm leading-5 text-muted-foreground">{o.desc}</span>
</div>
</label>
);
})}
</RadioGroup>Row of Box options with shared borders — no gap between them. -mr-px on all but the last. First card rounded-l-lg, last rounded-r-lg, middles no rounded. Active card z-10 + border-primary + bg-primary/10 draws over neighbors. Used in yuno-riskconditions-redesign's 'Then what to do when the rule matches' picker.
{/* Joined horizontal: no gap between cards, overlapping borders collapse via -mr-px.
First: rounded-l-lg. Last: rounded-r-lg. Middle: no rounded corners.
Active card: z-10 + border-primary + bg-primary/10 (draws over neighbors). */}
<RadioGroup defaultValue="option-2" className="grid grid-cols-4">
{items.map((o, i, arr) => {
const isFirst = i === 0;
const isLast = i === arr.length - 1;
return (
<label key={o.key} htmlFor={o.key}
className={cn(
"relative flex cursor-pointer items-start gap-3 border border-input bg-background p-3 transition-colors",
!isLast && "-mr-px",
isFirst && "rounded-l-lg",
isLast && "rounded-r-lg",
"hover:z-10 hover:border-foreground/20",
"has-[[data-state=checked]]:z-10 has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/10",
)}
>
<RadioGroupItem value={o.key} id={o.key} className="mt-0.5" />
<div className="grid gap-1.5">
<span className="text-sm font-medium leading-none text-foreground">{o.label}</span>
<span className="text-sm leading-5 text-muted-foreground">{o.desc}</span>
</div>
</label>
);
})}
</RadioGroup>Compact horizontal RadioField group without the Box wrapper. Use when the options are single-word and don't need a container (currency, small toggles inside a form).
<RadioGroup defaultValue="usd" className="flex flex-row items-center gap-6">
<RadioField value="usd" label="USD" />
<RadioField value="cop" label="COP" />
<RadioField value="mxn" label="MXN" />
<RadioField value="brl" label="BRL" />
</RadioGroup>Import
Copy this import line at the top of the file where you compose this atom.
import {
RadioGroup,
RadioGroupItem,
RadioField,
} from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";Props
Everything else from the underlying HTML or Radix primitive is forwarded via ...props.
| Prop | Type | Default | Description |
|---|---|---|---|
| RadioGroup.value / defaultValue | string | — | Controlled or uncontrolled selected value. Controlled requires onValueChange. |
| RadioGroup.onValueChange | (value: string) => void | — | Fires when the user picks a new option. |
| RadioGroup.orientation | "vertical" | "horizontal" | "vertical" | Layout direction. Vertical is the default; only go horizontal for short labels. |
| RadioGroup.disabled | boolean | false | Disables every item inside. Apply on individual RadioGroupItem to disable one. |
| RadioGroupItem.value | string | — | Value written when this option is picked. |
| RadioGroupItem.disabled | boolean | false | Disables just this option. Dims to opacity-50 and blocks click. |
| RadioField.label | React.ReactNode | — | Visible label next to the radio. Clicking the label toggles the option. |
| RadioField.description | React.ReactNode | — | Optional muted text-xs line below the label. |
| RadioField.containerClassName | string | — | Merged onto the outer flex container of the field. |
When to use
- 2-5 mutually exclusive options (choose only one).
- Settings where all options should be visible at once.
- Quick single-choice inside a form (payment method, currency, plan).
- Box variant: when the option itself should read as a clickable card.
When not to use
- Multi-select — use a Checkbox group.
- More than ~5 options — use a Select or Combobox.
- Only 2 options that are on/off — use a Switch.
Usage
- Show all options at once — that's the whole point of radios.
- Default to the safest / most common option.
- Use RadioField with description when options need extra context (plan features, permissions).
- Vertical layout is the default; only go horizontal when labels are super short.
- For Box variant, wrap the whole card in a <label> so the full surface is clickable.
- Don't hide options behind a Show more — put them all in view.
- Don't use radios when the choice is 'true/false' — that's a Switch.
- Don't leave the group without a default selection unless the field is optional.
- Don't mix Box and inline variants in the same group — pick one shape.
Related
Cross-links to atoms and patterns you may reach for next.
- CheckboxFor multi-select where more than one option can be chosen.
- SwitchFor a single on/off toggle rather than a mutually exclusive choice.
- SelectFor more than ~5 options where all shouldn't be visible at once.
- Toggle groupFor a segmented-control style single choice (buttons instead of radios).