React Radio Group Component Examples
Copy-paste React Radio Group examples: labeled options, controlled state, plan picker, square variant, and react-hook-form. Zero runtime deps. TypeScript only.
A radio group is the right affordance whenever the UI needs one choice from a small, fixed set — a notification channel, a billing plan, a shipping speed, a single-select survey answer. The Radio Group component in Drivn renders a flex flex-col gap-3 container with a real hidden <input type="radio"> behind each option, so the group posts in a plain HTML form and inherits the browser's radio behaviour. The whole tree lives in @/components/ui/radio-group.tsx, runs on a single React context, and imports nothing past react and the local cn() utility — no Radix, no icon package.
This page collects the call-site shapes that ship in real projects. Every example imports the component from @/components/ui/radio-group — the path the Drivn CLI installs under — and uses the same <RadioGroup><RadioGroup.Item /></RadioGroup> dot-notation shape. Every snippet is TypeScript because Drivn ships TypeScript-only.
The examples below cover a default labeled group, a controlled group whose value drives surrounding UI, a plan picker that uses the description prop for two-line options, a horizontal layout with the square variant, and a react-hook-form group validated with zod. The full component reference and props table live on the Radio Group docs page, and the shadcn/ui comparison sits on Drivn vs shadcn Radio Group. For a fresh install, see Installation and the Drivn CLI docs.
Default labeled group
The shortest useful Radio Group. Pass defaultValue on the root to pre-select an option, and give each RadioGroup.Item a unique value plus a label. The component renders a real <input type="radio"> behind each option and a text-sm font-medium label next to it; because the whole item is wrapped in a <label> element, clicking the text toggles the radio without any htmlFor wiring. The group is uncontrolled here — the selected value lives in the component's own React.useState, seeded from defaultValue.
This is the right starting shape for a settings row (notification channel, theme preference), a short single-select question, and any case where the parent does not need to read the value until form submit. Because the inputs are genuine radios, the group works inside a plain <form> and posts with FormData — add a name prop to the items and the selected value shows up in the submitted data without a hidden field. For a multi-select sibling where more than one option can be active, use the Drivn Checkbox instead.
1 import { RadioGroup } from "@/components/ui/radio-group" 2 3 export default function NotifyChannel() { 4 return ( 5 <RadioGroup defaultValue="email"> 6 <RadioGroup.Item value="email" label="Email" /> 7 <RadioGroup.Item value="sms" label="SMS" /> 8 <RadioGroup.Item value="push" label="Push" /> 9 </RadioGroup> 10 ) 11 }
Controlled radio group
When the selected value needs to drive surrounding UI — show a different panel per option, enable a button, recompute a price — lift the state into the parent with React.useState and pass value plus onValueChange to the root. The component becomes fully controlled: it renders whatever value the parent holds and reports every change through the callback, so the parent stays the single source of truth. The example below mirrors the selected channel into a line of text, but the same pattern feeds a conditional render, a fetch, or a derived calculation.
The 'use client' directive on this file is necessary because the call site itself runs React.useState — the Radio Group component does not force the boundary, the surrounding component does. onValueChange receives the new value string directly, so there is no event.target unwrapping. Controlled mode is also what the react-hook-form example further down relies on. For a value that the parent only reads at submit time and never mid-interaction, the uncontrolled default group above is the lighter choice.
1 'use client' 2 import * as React from 'react' 3 import { RadioGroup } from "@/components/ui/radio-group" 4 5 export default function ShippingSpeed() { 6 const [speed, setSpeed] = React.useState('standard') 7 8 return ( 9 <div className="space-y-3"> 10 <RadioGroup value={speed} onValueChange={setSpeed}> 11 <RadioGroup.Item value="standard" label="Standard (5 days)" /> 12 <RadioGroup.Item value="express" label="Express (2 days)" /> 13 <RadioGroup.Item value="overnight" label="Overnight" /> 14 </RadioGroup> 15 <p className="text-sm text-muted-foreground"> 16 Selected: {speed} 17 </p> 18 </div> 19 ) 20 }
Plan picker with descriptions
For a billing plan picker or any option list where each choice needs a supporting line, pass the description prop alongside label. The component stacks the description as a text-sm text-muted-foreground line under the bold label, and the whole two-line block stays inside the clickable <label> so tapping anywhere in the option selects it. This is the cleanest shape for plan tiers, permission levels, and shipping options where the price or detail belongs right under the name.
To turn each option into a bordered card tile, wrap the items in a Drivn Card or pass a className with border and padding utilities through RadioGroup.Item — the className merges via cn() onto the item wrapper. The example below keeps the plain two-line layout. When the built-in label-plus-description block is not enough — say you need a price badge aligned to the right — pass children instead of label/description and render any JSX you want; the component drops the built-in block and renders your content next to the radio. Pair the picker with a Drivn Badge for a "Most popular" marker.
1 import { RadioGroup } from "@/components/ui/radio-group" 2 3 export default function PlanPicker() { 4 return ( 5 <RadioGroup defaultValue="startup"> 6 <RadioGroup.Item 7 value="startup" 8 label="Startup" 9 description="Up to 5 team members, 10 GB storage" 10 /> 11 <RadioGroup.Item 12 value="business" 13 label="Business" 14 description="Up to 25 team members, 100 GB storage" 15 /> 16 <RadioGroup.Item 17 value="enterprise" 18 label="Enterprise" 19 description="Unlimited members, unlimited storage" 20 /> 21 </RadioGroup> 22 ) 23 }
Horizontal layout with the square variant
For a compact option row — a yes/no choice, a size selector, a short rating scale — pass orientation="horizontal" on the root. The component swaps its flex container from a column to a row (flex-row gap-4), so the options sit side by side instead of stacked. Pair it with variant="square" to render the radios with a rounded-[4px] frame and a rounded-[2px] inner dot instead of the default circle. A square radio reads as a distinct control while still signalling single-select, which helps in dense panels where circle and square shapes carry meaning.
The variant and orientation props are both read once on the root and shared to every item through context — there is no per-item override, which keeps a group visually consistent. The example below renders a horizontal square group for a t-shirt size picker. Horizontal layout works best with short labels; once an option needs a description line, the vertical default reads more cleanly. For longer option lists that would overflow a row, keep the vertical orientation or switch to the Drivn Select dropdown.
1 import { RadioGroup } from "@/components/ui/radio-group" 2 3 export default function SizePicker() { 4 return ( 5 <RadioGroup 6 defaultValue="m" 7 orientation="horizontal" 8 variant="square" 9 > 10 <RadioGroup.Item value="s" label="Small" /> 11 <RadioGroup.Item value="m" label="Medium" /> 12 <RadioGroup.Item value="l" label="Large" /> 13 <RadioGroup.Item value="xl" label="X-Large" /> 14 </RadioGroup> 15 ) 16 }
Validated radio group with react-hook-form
For a radio group inside a real form, wire it to react-hook-form with a Controller and validate the choice with zod. The zod schema marks the field required so a submit with no option selected surfaces an error message. The Controller bridges the form library to the controlled value / onValueChange shape the Radio Group already exposes — field.value flows into value and field.onChange flows into onValueChange, with no adapter code in between.
This is the production shape for a checkout step, a survey question, or any single-select field that must be answered before the form can submit. The zodResolver connects the schema to the form, and the error string renders under the group when validation fails. Because the Radio Group already speaks the controlled-input contract, the Controller wrapper is the only glue needed — the same pattern used for the Drivn Select and Checkbox in forms. See Drivn vs shadcn Radio Group for how the native-input underpinning compares with the Radix-backed alternative.
1 'use client' 2 import { useForm, Controller } from 'react-hook-form' 3 import { zodResolver } from '@hookform/resolvers/zod' 4 import { z } from 'zod' 5 import { RadioGroup } from "@/components/ui/radio-group" 6 import { Button } from "@/components/ui/button" 7 8 const schema = z.object({ 9 plan: z.string().min(1, 'Pick a plan to continue'), 10 }) 11 12 export default function PlanForm() { 13 const { control, handleSubmit, formState } = useForm({ 14 resolver: zodResolver(schema), 15 defaultValues: { plan: '' }, 16 }) 17 18 return ( 19 <form 20 onSubmit={handleSubmit((d) => console.log(d))} 21 className="space-y-3" 22 > 23 <Controller 24 name="plan" 25 control={control} 26 render={({ field }) => ( 27 <RadioGroup 28 value={field.value} 29 onValueChange={field.onChange} 30 > 31 <RadioGroup.Item value="startup" label="Startup" /> 32 <RadioGroup.Item value="business" label="Business" /> 33 </RadioGroup> 34 )} 35 /> 36 {formState.errors.plan && ( 37 <p className="text-sm text-destructive"> 38 {formState.errors.plan.message} 39 </p> 40 )} 41 <Button type="submit">Continue</Button> 42 </form> 43 ) 44 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
The component file ships with 'use client' because it calls React.useState to hold the selected value when used uncontrolled. The call site needs its own directive only when it owns interactive state — the controlled and react-hook-form examples both add 'use client' because the surrounding component runs useState or form hooks. The default and plan-picker examples are uncontrolled, so the group manages its own state and the call site does not add anything extra.
Add a name prop to each RadioGroup.Item. The component renders a real <input type="radio"> behind every option, and the name is spread onto that input through the forwarded InputHTMLAttributes. With a shared name, the browser groups the radios and the selected value appears in FormData on submit — no hidden mirror field needed. This is the practical payoff of Drivn building the group on native inputs rather than Radix buttons.
Pass the description prop alongside label on RadioGroup.Item. The component renders the description as a text-sm text-muted-foreground line stacked under the bold label, and the whole two-line block stays inside the clickable <label> element so a tap anywhere selects the option. When the built-in two-line layout is not enough — for example a price badge aligned right — pass children instead and render any JSX you want next to the radio.
Yes. Pass orientation="horizontal" on the RadioGroup root. The component swaps its flex container from a column to a row, so the options sit side by side. Horizontal layout suits short labels — a yes/no choice or a size picker. Once an option needs a description line, the vertical default (the root's standard flex flex-col gap-3) reads more cleanly. The orientation is read once on the root and shared to every item.
Pass variant="square" on the RadioGroup root. The component swaps the outer radio frame to rounded-[4px] and the inner selected dot to rounded-[2px] so the dot shape matches the frame. The default variant is circle. The variant is set once on the root and applies to every item through context — there is no per-item variant, which keeps a group visually consistent.

