Components

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.

Updated Jul 17, 2026 by Leonardo Posada

Anatomy

Up to 3 seats, basic reporting.

Up to 20 seats, advanced reporting.

  1. 1
    RadioGroup (root)

    Radix root that manages the selected value. Uncontrolled by default via defaultValue.

  2. 2
    RadioGroupItem

    The circular radio (size-4, border-input, rounded-full). Checked state shows a filled inner dot (Circle weight='fill', size-2, text-primary).

  3. 3
    Label

    The clickable text next to the radio. Uses htmlFor to bind to the radio's id (RadioField wires it for you).

  4. 4
    Description

    Optional muted text-xs line below the label. Adds context when options aren't self-explanatory.

  5. 5
    Box 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.

Box only
<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>
States
<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.

Label only
<RadioGroup defaultValue="monthly">
  <RadioField value="monthly" label="Monthly" />
  <RadioField value="annual" label="Annual" />
  <RadioField value="lifetime" label="Lifetime" />
</RadioGroup>
Label + description

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.

Default · Selected
{/* 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>
Box variant for pickers
When the option itself should read as a clickable card (plan picker, payment method, feature selection), reach for the Box variant. It's the canonical Yuno pattern for those choices — the full card is clickable, selected state gets primary border + tinted bg, and the radio dot on the left keeps semantics correct.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Vertical, spaced

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).

Horizontal, spaced

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).

Vertical, joined (no gap)

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>
Horizontal, joined (no gap)

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>
Inline (no boxes)

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.

The Box variant is a recipe (composition) — no extra import. Wrap each option in a <label> with the classes shown above.
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.

PropTypeDefaultDescription
RadioGroup.value / defaultValuestringControlled or uncontrolled selected value. Controlled requires onValueChange.
RadioGroup.onValueChange(value: string) => voidFires 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.disabledbooleanfalseDisables every item inside. Apply on individual RadioGroupItem to disable one.
RadioGroupItem.valuestringValue written when this option is picked.
RadioGroupItem.disabledbooleanfalseDisables just this option. Dims to opacity-50 and blocks click.
RadioField.labelReact.ReactNodeVisible label next to the radio. Clicking the label toggles the option.
RadioField.descriptionReact.ReactNodeOptional muted text-xs line below the label.
RadioField.containerClassNamestringMerged 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

Do
  • 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
  • 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).