Radio group

A set of checkable buttons—known as radio buttons—where only one can be selected at a time.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/radio-group.json
Usage
Pass an `options` array. Use `variant="card"` for bordered cards with optional `description` / `meta` per option.
TSImport
import { RadioGroup } from "@/components/ui/radio-group";
TSExample
import { RadioGroup } from "@/components/ui/radio-group";

export function Example() {
  return (
    <RadioGroup
      defaultValue="comfortable"
      options={[
        { value: "default", label: "Default" },
        { value: "comfortable", label: "Comfortable" },
        { value: "compact", label: "Compact (disabled)", disabled: true },
      ]}
    />
  );
}
Composition
Anatomy of the RadioGroup primitive.
RadioGroup                       (single component — pass options as data)
├── type="default" | "card" | "pill" | "chip" | "tile" | "image"
├── options={[
│     { value, label, caption?, meta?, icon?, disabled? },
│     …
│   ]}
└── otherOption={{ value, label, caption?, placeholder? }}
    + otherValue / onOtherValueChange / defaultOtherValue

variant rendering:
  default → bare radios + labels (rows)
  card    → full-width bordered card per option (label + caption + meta)
  pill    → content-width compact card (radio + label, wraps inline)
  chip    → borderless tile (label + optional meta line, wraps inline)
  tile    → large icon-led card (icon + label + caption, centered)
  image   → image picker — the image is the tile, radio dot in its corner
            (add `bento` for mixed-shape images)

For multi-select, see <MultiSelect /> in @/components/ui/multi-select
(documented on the /multi-select page) — same flat options shape, with
type="cards" or type="chip-group".

Slot exports — RadioGroupItem, RadioGroupCard, RadioGroupCardLabel,
RadioGroupCardDescription — remain as the escape hatch for fully custom
layouts.
Basic
Card group
Each option is a bordered card with a radio on the left. Border tints primary when selected.
Card group — 2-column wrap
Same card treatment, laid out in a responsive 2-column grid that collapses to a single column on narrow screens.
Card group with detail field
Flag any card option with `requiresDetail: true` (plus an optional `detailLabel`) and selecting it reveals a required textarea nested directly beneath it — indented and tied back with an elbow connector. Pass `detailValues` / `onDetailChange` to read the text (a `value → text` map). Card variant only, at `cols` 1 or 2.
Cards with badges + thumbnails
Pass `lastUsed: true` on any card option to float a built-in amber “Last used” pill on its top border — no `<Badge>` import or JSX needed. Pass `badge` for any other pill, and `image` (or `swatch`) for a leading square thumbnail.
Rich plan cards
Cards carry richer content through five option props: `tags` (an overlapping, hover-animated circular icon/image stack + labels under the caption), `aside` (a block pinned to the right edge — price stacks, badge columns), `titleBadge` (a badge or link inline to the right of the label), `features` (a check-list that expands open inside the card when the option is selected — `grid-template-rows` 0fr→1fr, transitions.dev #01) and `featuresTitle` above it.
Card group — content-width wrap
Cards size to their contents and flow left-to-right, wrapping onto new rows as the container narrows.
Pill group that wraps with Other
`type="pill"` with a circular radio indicator before each label. Pass `wrap` so pills size to their content and flow onto new rows; the trailing `otherOption` morphs into an inline input when selected — blur with a blank value to clear, or a filled value to collapse it to a chip showing the typed text. Mirrors the multi-select chip group.
Pill group — 3 columns with Other
Same pill treatment, but pass `cols={3}` for a fixed three-column grid of equal-width pills instead of `wrap`. In columns mode the radio dot pins to the left and the label centers; the inline `otherOption` fills its column when selected.
Chip group
Compact grid of options; the whole chip is clickable and the radio is visually hidden.

Select shoe size

With Other input
Selecting the last option reveals an Input so the user can enter a custom value.

How did you hear about us?

Card group with Other
The last card reveals an Input for a custom answer, right inside the card.

What team are you on?

Plan cards
Icon-led vertical cards; the radio input is hidden and the whole card toggles selection.
Error state
Pass `error` to the RadioGroup. A boolean marks items invalid (red ring); a string also renders the inline `text-xs mt-1.5 text-red-500` message — same treatment as RichTextEditor and FileUpload.
With caption
Pass `caption` to render helper copy under the label. Error wins when both are present.

We'll only use this for important project updates.

Image tiles
`type="image"` turns the group into an image picker: the image is the whole tile, with a radio dot over its corner. Single choice — picking a tile moves the check.
Bento image tiles
Add `bento` for images of every shape: each keeps its own aspect ratio and the columns balance to end on one line. Pass `aspectRatio` on an option when you know it; otherwise the image is measured as it loads.
API Reference
Props exposed by the RadioGroup primitive and its items.

RadioGroup

PropTypeDefaultDescription
optionsRadioGroupOption[]—List of options to render. Without this prop, RadioGroup stays compound (drop in `<RadioGroupItem>` children manually).
type"default" | "card" | "pill" | "chip" | "tile" | "image""default"Variant. `default` is bare radios. `card` wraps each option in a bordered row with optional `meta` / `description`. `pill` and `chip` are single-row h-9 selectors. `tile` is a square tile with a centered label. `image` is an image picker — the image is the whole tile. (Legacy alias: `variant` — still accepted.)
colsnumber—Number of columns. For `card` / `tile`, omitting it wraps responsively (auto-fill, min 16rem per card). For `pill`, sets a fixed grid of equal-width pills (e.g. `cols={2}`); ignored when `wrap` is set.
bentobooleanfalse`image` only. Bento grid for mixed-shape images: each keeps its own aspect ratio and the columns balance to end on one line.
wrapbooleanfalse`pill` only. Flow pills in a wrapping inline row sized to each label instead of a fixed grid. Wins over `cols`, and is the default for `pill` when neither is set.
valuestring—Controlled selected value. Pair with `onValueChange`.
defaultValuestring—Uncontrolled initial value.
onValueChange(value: string) => void—Fires when the selected radio changes.
namestring—Form field name (rendered as a hidden input).
disabledboolean—Disables every radio in the group.
errorboolean | string—Error state. `true` marks items invalid (red ring); a string also renders the inline `text-xs mt-1.5 text-red-500` message below the group.
captionReactNode—Helper copy shown below the group via FieldHint. Error wins when both are present.
classNamestring—Override the wrapper layout (defaults to `grid w-full gap-2`).
detailValuesRecord<string, string>—Card variant only. Controlled `option.value → detail text` map for options flagged `requiresDetail`.
defaultDetailValuesRecord<string, string>—Card variant only. Uncontrolled initial detail-text map.
onDetailChange(next: Record<string, string>) => void—Card variant only. Fires when the revealed detail textarea changes.

RadioGroupOption

PropTypeDefaultDescription
valuestring—Value reported when the option is selected.
labelReactNode—Visible label rendered next to the radio.
captionReactNode—Helper text — only rendered when `type="card"`.
metaReactNode—Right-aligned secondary text (e.g. `"Free"`, `"$5"`) — card variant only.
requiresDetailboolean—Card variant only. Selecting this option reveals a required textarea beneath it, tied back with an elbow connector. Honored at `cols` 1 or 2.
detailLabelstring—Label above the revealed detail textarea. Defaults to "Provide more details".
detailPlaceholderstring—Placeholder for the revealed detail textarea.
lastUsedbooleanfalseCard variant only. Floats a built-in amber "Last used" pill on the card's top border — no `<Badge>` import needed.
badgeReactNode—Card variant only. Custom pill floated on the card's top border (e.g. `<Badge type="solid" size="xxs">Most popular</Badge>`). Renders alongside `lastUsed` when both are set.
imagestring—Card variant: image URL rendered as the leading square thumbnail. Image variant: the tile itself.
aspectRationumber—Image variant with `bento`: the image's width ÷ height. Left out, the tile measures its image as it loads.
imageAltstring—Alt text for `image`.
swatchReactNode—Card variant only. Custom leading visual (gradient, icon, …) — wins over `image`.
tags{ icon?: ReactNode; image?: string; label?: ReactNode }[]—Card variant only. Icon/label row under the caption — the icons render as an overlapping circular stack (hovering one lifts it and shifts its neighbors with a distance falloff, transitions.dev #11), labels joined with `·`. Pass `image` (e.g. `/brand/departments/circle-graphic.svg`) for a pre-drawn circular asset instead of `icon` JSX — it renders full-bleed with no border ring, and the label doubles as its `alt`/`title`.
asideReactNode—Card variant only. Block pinned to the card's right edge (price stack, badge column, …). Rendered as-is.
titleBadgeReactNode—Card variant only. Badge or link rendered inline just to the right of the card's label.
featuresReactNode[]—Card variant only. Check-list that expands open inside the card when the option is selected (animated 0fr→1fr grid reveal). Cards with `features`, `aside`, or `tags` also pin the radio to the top-left instead of centering it.
featuresTitleReactNode—Small uppercase heading above the revealed `features` list.
disabledboolean—Disable this individual option.