Skip to content
Drivn
8 min read

Drivn vs shadcn/ui — Radio Group Compared

Compare Drivn Radio Group vs shadcn/ui — a native-input compound versus a Radix button wrapper. Both copy-paste, one ships zero runtime deps.

Drivn's Radio Group and shadcn/ui's Radio Group both render the same affordance — a set of mutually exclusive options where picking one clears the rest — but they reach it through different machinery. shadcn/ui wraps @radix-ui/react-radio-group and ships two exports, RadioGroup and RadioGroupItem, where the item is a Radix button styled into a circle with a Circle icon for the selected dot. Drivn ships a single file at @/components/ui/radio-group.tsx that holds a React context, a RadioGroupRoot, and a forwardRef Item, joined into a dot-notation API through Object.assign(RadioGroupRoot, { Item }).

The practical split is native input versus Radix button. Each Drivn Item renders a real hidden native <input type="radio"> marked peer sr-only, so the focus ring, the form semantics, and the checked state come from the platform rather than from a Radix roving-tabindex layer. The inputs post in a plain HTML form and pair with FormData without a hidden field. shadcn's items are Radix buttons that manage arrow-key navigation and roving tabindex internally, which is the more complete WAI-ARIA radiogroup contract — but it is also a Radix package in the bundle and a 'use client' boundary.

This page walks the runtime footprint, the keyboard model, the built-in label and description props, the circle and square variants, and the call-site shape so you can pick the flavor that fits a new project.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying primitiveNative <input type="radio"> + React context@radix-ui/react-radio-group
Runtime UI dependenciescn() utility only@radix-ui/react-radio-group + lucide-react
API shapeDot notation — RadioGroup.ItemTwo exports — RadioGroup + RadioGroupItem
Keyboard navigationNative radio — Tab into group, arrows moveRadix roving tabindex
Built-in label + descriptionNo — pair with separate <Label>
Indicator variantscircle and squarecircle only
Orientation propvertical and horizontalorientation passed to Radix
Controlled + uncontrolleddefaultValue / value + onValueChangedefaultValue / value + onValueChange
Plain HTML form submitYes — real radio inputsRadix renders a hidden input
LicenseMITMIT
Copy-paste install

The runtime footprint

shadcn/ui's Radio Group is a thin layer over @radix-ui/react-radio-group. The component file imports RadioGroupPrimitive from the Radix package and Circle from lucide-react, then re-exports RadioGroupPrimitive.Root as RadioGroup and wraps RadioGroupPrimitive.Item with a RadioGroupPrimitive.Indicator child that renders the filled Circle dot. The Radix package implements the roving-tabindex keyboard model, the controlled-vs-uncontrolled semantics, and the ARIA radiogroup wiring.

Drivn's Radio Group ships one file that imports React and the local cn() utility — no Radix, no icon package. The selected value lives in a React.useState (uncontrolled) or a controlled value prop, shared down through a Ctx provider so each Item knows whether it is the active option. The styles object holds every class the component uses and is reproduced verbatim from the registry below. The file carries a 'use client' directive because it calls useState, the same as the shadcn flavor — the difference is the dependency tree, not the client boundary. The same dependency-shape trade-off appears in Drivn vs shadcn Checkbox and Drivn vs shadcn Badge — Drivn keeps the primitive local, shadcn delegates to Radix.

1// Drivn — styles object, no Radix (verbatim from registry)
2const styles = {
3 group: 'flex flex-col gap-3',
4 horizontal: 'flex-row gap-4',
5 item: 'flex items-start gap-2 cursor-pointer',
6 radio: cn(
7 'aspect-square w-4 mt-[3px] border border-input shadow-xs',
8 'transition-[color,box-shadow] flex-shrink-0',
9 'flex items-center justify-center',
10 'peer-focus-visible:ring-[3px]',
11 'peer-focus-visible:ring-ring/50',
12 'peer-focus-visible:border-ring'
13 ),
14 checked: 'bg-foreground border-foreground',
15 indicator: 'w-2 h-2 bg-background',
16 label: 'text-sm font-medium text-foreground select-none',
17 description: 'text-sm text-muted-foreground select-none',
18 variants: {
19 circle: 'rounded-full',
20 square: 'rounded-[4px]',
21 },
22 indicators: {
23 circle: 'rounded-full',
24 square: 'rounded-[2px]',
25 },
26}

Native radio input vs Radix button

This is the real divergence. Each Drivn Item renders inside a <label> and contains a genuine native <input type="radio"> marked peer sr-only — visually hidden but still a real form control. The visible circle is a sibling <span> styled by the radio class, and the focus ring is wired through peer-focus-visible:ring-[3px] so it lights up when the hidden input takes focus. Because the inputs are real radios, they participate in plain HTML form submission, they pair with FormData without a hidden mirror field, and the browser supplies the same-name grouping and arrow-key navigation that the platform has shipped for radios since forever.

shadcn/ui's items are Radix buttons, not inputs. Radix implements the WAI-ARIA radiogroup pattern with roving tabindex — only the selected button is in the tab order, and arrow keys move selection within the group. Radix also renders its own hidden input for form posts. The Radix model is the more thorough ARIA contract and handles edge cases (RTL arrow direction, typeahead) that the native flavor leaves to the browser. The trade-off is the same one this comparison keeps returning to: Drivn leans on the platform and ships a smaller file, shadcn leans on Radix and ships a more exhaustive interaction layer. The native-element framing also drives Drivn vs shadcn Checkbox.

1// Drivn — Item renders a real hidden radio input (verbatim)
2<input
3 ref={ref}
4 type="radio"
5 className="peer sr-only"
6 checked={isChecked}
7 disabled={isDisabled}
8 onChange={() => {
9 if (!isDisabled) ctx.onSelect(value)
10 }}
11 {...props}
12/>
13<span className={cn(styles.radio, styles.variants[ctx.variant], isChecked && styles.checked)}>
14 {isChecked && <span className={cn(styles.indicator, styles.indicators[ctx.variant])} />}
15</span>

Built-in label and description

Drivn's Item accepts label and description props and renders them itself. Pass label="Email" and the component drops a text-sm font-medium label next to the radio; add description="Get notified by email" and it stacks a text-sm text-muted-foreground line under the label. Because the whole Item is wrapped in a <label> element, clicking either the label or the description toggles the radio without any htmlFor/id plumbing. When the built-in two-line layout is not enough, pass children instead and the component renders your custom content in place of the label block.

shadcn/ui's RadioGroupItem ships the circle only. Labels are a separate concern: you render a shadcn <Label htmlFor={id}> next to each RadioGroupItem and wire the id/htmlFor pair by hand, usually inside a flex row. That is more markup per option but also more layout freedom — the label is a plain element you place anywhere. Drivn's built-in label/description props cover the common settings-list and plan-picker shapes in one line; shadcn's separate-Label approach is the escape hatch Drivn reaches with children. See the Radio Group examples page for the label, description, and custom-children call sites side by side.

Circle and square variants

Drivn's Radio Group exposes a variant prop with two values, circle (default) and square. The variant is read on the root and shared through the context, so every Item in the group picks it up — there is no per-item variant. Under the hood the variants map swaps the outer radio between rounded-full and rounded-[4px], and the indicators map swaps the inner selected dot between rounded-full and rounded-[2px] so the dot shape matches the frame. A square radio reads as a distinct control from a checkbox while still signalling single-select, which is useful in dense settings panels where round and square shapes carry meaning.

shadcn/ui's Radio Group is circle only. The Radix indicator renders a Circle icon and the wrapper className rounds the button with rounded-full; a square variant means editing the component file to swap both. Drivn also exposes an orientation prop (vertical default, horizontal) that flips the root flex container from a column to a row through the horizontal style key — handy for a compact yes/no or short option row. Both libraries support controlled and uncontrolled use through the same defaultValue / value + onValueChange shape, so the state model is identical even though the styling surface differs.

1// Drivn — variant maps swap frame and dot shape (verbatim)
2variants: {
3 circle: 'rounded-full',
4 square: 'rounded-[4px]',
5},
6indicators: {
7 circle: 'rounded-full',
8 square: 'rounded-[2px]',
9},

Call-site shape and props

Drivn uses a dot-notation compound. One import — import { RadioGroup } from "@/components/ui/radio-group" — gives you RadioGroup and RadioGroup.Item. A group reads <RadioGroup defaultValue="email"><RadioGroup.Item value="email" label="Email" />...</RadioGroup>. The root accepts defaultValue, value, onValueChange, orientation, variant, and disabled; disabling the group disables every item, and an individual Item can carry its own disabled to gray out one option. The Item also forwards a ref to the underlying input and spreads any extra InputHTMLAttributes, so name, required, and other native attributes flow straight through.

shadcn/ui uses two named imports — RadioGroup and RadioGroupItem — and the call site interleaves each RadioGroupItem with a <Label>. The state props match Drivn's (defaultValue, value, onValueChange), so migrating the state logic is a no-op; the difference is the per-option markup. For most forms — notification preferences, plan selection, a single-select question — the Drivn call site is shorter because the label lives on the Item. For layouts that need the label decoupled from the control, the shadcn approach (or Drivn's children escape hatch) gives more room. See Radio Group examples for the controlled, react-hook-form, and card-style call sites.

1// Drivn — one import, dot-notation compound
2import { RadioGroup } from "@/components/ui/radio-group"
3
4export default function NotifyChannel() {
5 return (
6 <RadioGroup defaultValue="email">
7 <RadioGroup.Item value="email" label="Email" />
8 <RadioGroup.Item value="sms" label="SMS" />
9 <RadioGroup.Item value="push" label="Push" />
10 </RadioGroup>
11 )
12}

When each wins

Pick shadcn/ui's Radio Group when the project already standardizes on Radix elsewhere (Dialog, Tooltip, Select) and you want the full WAI-ARIA radiogroup interaction layer without writing any of it — roving tabindex, RTL-aware arrow directions, and the Radix-managed hidden input for form posts. Complex forms, internationalized apps, and teams that prefer one consistent primitive vendor across every control all live in this column. The cost is one more Radix package and the lucide-react icon import in the bundle, which most shadcn projects already pay for elsewhere.

Pick Drivn's Radio Group when the priority is a small dependency tree and a call site that reads top-to-bottom. The native <input type="radio"> underneath means the group works in a plain HTML form, posts with FormData cleanly, and inherits browser radio behaviour for free. The built-in label and description props collapse the common settings-list shape to one line per option, and the circle/square variants plus horizontal orientation cover the layout range without extra markup. Pair the Radio Group with the Drivn Card for plan-picker tiles, the Label for standalone form fields, and react-hook-form plus zod for validation. See the Radio Group examples page for the call-site shapes that fit each scenario.

Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Each RadioGroup.Item renders a real <input type="radio"> marked peer sr-only — visually hidden but a genuine form control. The visible circle is a sibling <span>, and the focus ring lights up through the peer-focus-visible: selector when the hidden input takes focus. Because the inputs are real radios, the group posts in a plain HTML form and pairs with FormData without a hidden mirror field. The shadcn/ui flavor renders Radix buttons instead and adds its own hidden input for form posts.

Yes. The component calls React.useState to hold the selected value when used uncontrolled, and useState is a client-only API, so the registry source ships with a 'use client' directive. The shadcn/ui flavor also carries the directive because it wraps a Radix client component. The client boundary is the same for both libraries — the difference between them is the dependency tree, not whether the component runs on the client.

Pass the label and description props directly on RadioGroup.Item. The component renders the label as a text-sm font-medium line and the description as a text-sm text-muted-foreground line stacked under it. Because the whole item is wrapped in a <label> element, clicking either piece toggles the radio without any htmlFor/id wiring. When the built-in two-line layout is not enough, pass children instead and the component renders your custom content in place of the label block.

Yes. Pass variant="square" on the RadioGroup root. The variants map swaps the outer frame from rounded-full to rounded-[4px], and the indicators map swaps the inner selected dot from rounded-full to rounded-[2px] so the dot shape matches the frame. The variant is read once on the root and shared to every item through context — there is no per-item variant. The shadcn/ui flavor is circle only, so a square radio there means editing the component file.

Pass orientation="horizontal" on the RadioGroup root. The component swaps the root flex container from a column (flex flex-col gap-3) to a row (flex-row gap-4) through the horizontal style key. The horizontal layout fits compact yes/no choices and short option rows. The default is vertical, which stacks each option in its own row and suits longer labels and descriptions. Both orientations share the same context and selection logic.