Radio Group
A set of checkable buttons where no more than one of the buttons can be checked at a time. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<RadioGroup defaultValue="next">
<Label>
<Radio value="next" /> Next.js
</Label>
<Label>
<Radio value="vite" /> Vite
</Label>
<Label>
<Radio value="astro" /> Astro
</Label>
</RadioGroup>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/radio-group
Usage
import { Label } from "@/components/ui/label"
import { Radio, RadioGroup } from "@/components/ui/radio-group"<RadioGroup defaultValue="next">
<Label>
<Radio value="next" /> Next.js
</Label>
<Label>
<Radio value="vite" /> Vite
</Label>
<Label>
<Radio value="astro" /> Astro
</Label>
</RadioGroup>Examples
For accessible labelling and validation, prefer using the Field component to wrap radio buttons. See the related example: Radio Group field.
Disabled
Renders the entire group or individual options as disabled, preventing selection while keeping options visible.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<RadioGroup defaultValue="next">
<Label>
<Radio value="next" /> Next.js
</Label>
<Label>
<Radio disabled value="vite" /> Vite (disabled)
</Label>
<Label>
<Radio value="astro" /> Astro
</Label>
</RadioGroup>
);
}
With Description
Each radio option carries a short description below its label to clarify the difference between choices.
Basic features for personal use.
Advanced tools for professionals.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<RadioGroup defaultValue="r-1">
<div className="flex items-start gap-2">
<Radio id="r-1" value="r-1" />
<div className="flex flex-col gap-1">
<Label htmlFor="r-1">Free</Label>
<p className="text-muted-foreground text-xs">
Basic features for personal use.
</p>
</div>
</div>
<div className="flex items-start gap-2">
<Radio id="r-2" value="r-2" />
<div className="flex flex-col gap-1">
<Label htmlFor="r-2">Pro</Label>
<p className="text-muted-foreground text-xs">
Advanced tools for professionals.
</p>
</div>
</div>
</RadioGroup>
);
}
Card Style
Each option is rendered as a selectable card — clicking anywhere on the card selects the radio and highlights the card border.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<RadioGroup defaultValue="r-1">
<Label className="flex items-start gap-2 rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50">
<Radio value="r-1" />
<div className="flex flex-col gap-1">
<p>Email</p>
<p className="text-muted-foreground text-xs">
Receive notifications via email.
</p>
</div>
</Label>
<Label className="flex items-start gap-2 rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50">
<Radio value="r-2" />
<div className="flex flex-col gap-1">
<p>SMS</p>
<p className="text-muted-foreground text-xs">
Receive notifications via text message.
</p>
</div>
</Label>
</RadioGroup>
);
}
Colored Variants
Radio options with colored indicator dots or backgrounds to visually distinguish categories such as status or priority levels.
import { Field, FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export function Pattern() {
return (
<RadioGroup className="w-fit" defaultValue="blue">
<Field className="flex-row">
<RadioGroupItem
className="border-blue-500 data-checked:border-blue-500 [&_.cn-radio-group-indicator]:text-blue-500"
id="color-blue"
value="blue"
/>
<FieldLabel htmlFor="color-blue">Blue</FieldLabel>
</Field>
<Field className="flex-row">
<RadioGroupItem
className="border-green-500 data-checked:border-green-500 [&_.cn-radio-group-indicator]:text-green-500"
id="color-green"
value="green"
/>
<FieldLabel htmlFor="color-green">Green</FieldLabel>
</Field>
<Field className="flex-row">
<RadioGroupItem
className="border-yellow-500 data-checked:border-yellow-500 [&_.cn-radio-group-indicator]:text-yellow-500"
id="color-yellow"
value="yellow"
/>
<FieldLabel htmlFor="color-yellow">Yellow</FieldLabel>
</Field>
</RadioGroup>
);
}
With Legend
Wraps the group in a Fieldset with a visible FieldsetLegend to provide a group-level accessible name.
import { Field, FieldLabel } from "@/components/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export function Pattern() {
return (
<Fieldset className="w-full max-w-xs">
<FieldsetLegend>Battery Level</FieldsetLegend>
<RadioGroup defaultValue="medium">
<Field className="flex-row">
<RadioGroupItem id="battery-high" value="high" />
<FieldLabel htmlFor="battery-high">High</FieldLabel>
</Field>
<Field className="flex-row">
<RadioGroupItem id="battery-medium" value="medium" />
<FieldLabel htmlFor="battery-medium">Medium</FieldLabel>
</Field>
<Field className="flex-row">
<RadioGroupItem id="battery-low" value="low" />
<FieldLabel htmlFor="battery-low">Low</FieldLabel>
</Field>
</RadioGroup>
</Fieldset>
);
}
In Card With Description
Each radio option is a full card containing a title, description, and a trailing radio indicator — a plan or tier selection pattern.
Basic features for personal use.
Advanced tools for professionals.
"use client";
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
import { Label } from "../ui/label";
export function Pattern() {
const [selected, setSelected] = useState("r-1");
return (
<RadioGroup onValueChange={setSelected} value={selected}>
<Card
className={`flex flex-row items-start gap-2 rounded-lg p-3 transition-colors ${selected === "r-1" ? "bg-primary/20" : ""}`}
>
<Radio id="r-1" value="r-1" />
<div className="flex flex-col gap-1">
<Label htmlFor="r-1">Free</Label>
<p className="text-muted-foreground text-xs">
Basic features for personal use.
</p>
</div>
</Card>
<Card
className={`flex flex-row items-start gap-2 rounded-lg p-3 transition-colors ${selected === "r-2" ? "bg-primary/20" : ""}`}
>
<Radio id="r-2" value="r-2" />
<div className="flex flex-col gap-1">
<Label htmlFor="r-2">Pro</Label>
<p className="text-muted-foreground text-xs">
Advanced tools for professionals.
</p>
</div>
</Card>
</RadioGroup>
);
}
In Card With Icons
Radio cards that include an icon alongside the label, making visual scanning easier in feature or category pickers.
"use client";
import { Mail, MessageSquareCheck, Phone } from "lucide-react";
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
import { Label } from "../ui/label";
export function Pattern() {
const [selected, setSelected] = useState("r-1");
return (
<RadioGroup onValueChange={setSelected} value={selected}>
<Card
className={`flex flex-row items-start justify-between gap-20 rounded-lg p-3 transition-colors ${selected === "r-1" ? "bg-primary/20" : ""}`}
>
<div className="flex flex-row gap-2">
<Mail className="size-4" />
<Label htmlFor="r-1">Email</Label>
</div>
<Radio id="r-1" value="r-1" />
</Card>
<Card
className={`flex flex-row items-start justify-between gap-2 rounded-lg p-3 transition-colors ${selected === "r-2" ? "bg-primary/20" : ""}`}
>
<div className="flex flex-row gap-2">
<Phone className="size-4" />
<Label htmlFor="r-2">Phone</Label>
</div>
<Radio id="r-2" value="r-2" />
</Card>
<Card
className={`flex flex-row items-start justify-between gap-2 rounded-lg p-3 transition-colors ${selected === "r-3" ? "bg-primary/20" : ""}`}
>
<div className="flex flex-row gap-2">
<MessageSquareCheck className="size-4" />
<Label htmlFor="r-3">Message</Label>
</div>
<Radio id="r-3" value="r-3" />
</Card>
</RadioGroup>
);
}
Form Integration
Wraps the radio group in Form and Field for required-state validation and visible error messaging on submit.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldItem, FieldLabel } from "@/components/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
import { Form } from "@/components/ui/form";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Selected: ${formData.get("frameworks")}`);
};
return (
<Form className="flex w-full max-w-40 flex-col gap-4" onSubmit={onSubmit}>
<Field
className="gap-2"
name="frameworks"
render={(props) => <Fieldset {...props} />}
>
<FieldsetLegend className="font-medium text-sm">
Frameworks
</FieldsetLegend>
<RadioGroup defaultValue="next">
<FieldItem>
<FieldLabel>
<Radio value="next" /> Next.js
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="vite" /> Vite
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="astro" /> Astro
</FieldLabel>
</FieldItem>
</RadioGroup>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Horizontal Layout
Displays radio options inline in a horizontal flex row — useful for short option sets like framework or size pickers.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<RadioGroup
className="flex-row flex-wrap gap-x-6 gap-y-2"
defaultValue="react"
>
<Label>
<Radio value="react" /> React
</Label>
<Label>
<Radio value="vue" /> Vue
</Label>
<Label>
<Radio value="angular" /> Angular
</Label>
<Label>
<Radio value="svelte" /> Svelte
</Label>
<Label>
<Radio value="solid" /> Solid
</Label>
</RadioGroup>
);
}
Shipping Options
Each delivery method is a card row with label, estimated timing, and price — selecting one highlights the card border.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const options = [
{
description: "5–7 business days",
label: "Standard",
price: "Free",
value: "standard",
},
{
description: "2–3 business days",
label: "Express",
price: "$9.99",
value: "express",
},
{
description: "Next business day",
label: "Overnight",
price: "$24.99",
value: "overnight",
},
];
export default function Particle() {
return (
<RadioGroup className="w-full max-w-sm" defaultValue="standard">
{options.map(({ description, label, price, value }) => (
<Label
className="flex cursor-pointer items-center gap-3 rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50"
key={value}
>
<Radio value={value} />
<div className="flex flex-1 items-center justify-between">
<div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">{label}</span>
<span className="text-muted-foreground text-xs">
{description}
</span>
</div>
<span className="font-medium text-sm">{price}</span>
</div>
</Label>
))}
</RadioGroup>
);
}
Theme Selector
Icon-centric radio cards for light, dark, and system theme — the radio indicator sits at the top-right of each card.
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const themes = [
{ Icon: SunIcon, label: "Light", value: "light" },
{ Icon: MoonIcon, label: "Dark", value: "dark" },
{ Icon: MonitorIcon, label: "System", value: "system" },
];
export default function Particle() {
return (
<RadioGroup className="flex-row gap-3" defaultValue="system">
{themes.map(({ Icon, label, value }) => (
<Label
className="relative flex w-24 cursor-pointer flex-col items-center gap-3 rounded-lg border p-4 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50"
key={value}
>
<Radio className="absolute top-3 right-3" value={value} />
<Icon aria-hidden="true" className="size-5" />
<span className="text-sm">{label}</span>
</Label>
))}
</RadioGroup>
);
}
Availability Status
A status-picker radio group with colored dots and short descriptions for online, away, busy, and offline states.
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const statuses = [
{
color: "bg-green-500",
description: "Visible to everyone",
label: "Online",
value: "online",
},
{
color: "bg-yellow-500",
description: "Will reply when back",
label: "Away",
value: "away",
},
{
color: "bg-red-500",
description: "Minimizing interruptions",
label: "Busy",
value: "busy",
},
{
color: "bg-foreground/30",
description: "Appears offline to others",
label: "Offline",
value: "offline",
},
];
export default function Particle() {
return (
<RadioGroup className="w-full max-w-xs" defaultValue="online">
{statuses.map(({ color, description, label, value }) => (
<Label
className="flex cursor-pointer items-center gap-3 rounded-lg p-2 hover:bg-accent/50"
key={value}
>
<Radio value={value} />
<span
aria-hidden="true"
className={`size-2.5 shrink-0 rounded-full ${color}`}
/>
<div className="flex flex-col">
<span className="font-medium text-sm">{label}</span>
<span className="text-muted-foreground text-xs">{description}</span>
</div>
</Label>
))}
</RadioGroup>
);
}
Billing Frequency
Three billing cycles (monthly, quarterly, annually) with savings badges on the discounted options.
Billing frequency
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const frequencies = [
{
badge: null,
description: "Pay each month",
label: "Monthly",
value: "monthly",
},
{
badge: "Save 10%",
description: "Billed every quarter",
label: "Quarterly",
value: "quarterly",
},
{
badge: "Save 25%",
description: "Billed once a year",
label: "Annually",
value: "annually",
},
];
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<p className="font-medium text-sm">Billing frequency</p>
<RadioGroup defaultValue="monthly">
{frequencies.map(({ badge, description, label, value }) => (
<Label
className="flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-accent/50 has-data-checked:border-primary/48 has-data-checked:bg-accent/50"
key={value}
>
<div className="flex items-center gap-3">
<Radio value={value} />
<div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">{label}</span>
<span className="text-muted-foreground text-xs">
{description}
</span>
</div>
</div>
{badge && <Badge variant="secondary">{badge}</Badge>}
</Label>
))}
</RadioGroup>
</div>
);
}

