Radio Group
Mutually exclusive options with clear active state and descriptive labels.
01 Live Demo
02 Code Snippet
src/ui/RadioGroup.tsx
import { RadioGroup } from "@/ui/RadioGroup"; <RadioGroup.Root value={plan} onValueChange={setPlan}> <RadioGroup.Item value="starter" label="Starter" description="Best for side projects" /> <RadioGroup.Item value="pro" label="Pro" description="For production apps" /> <RadioGroup.Item value="enterprise" label="Enterprise" description="Advanced controls" /> </RadioGroup.Root>
03 Copy-Paste (Single File)
RadioGroup.tsx
import { createContext, useContext, useState, type ReactNode } from "react"; type Ctx = { value: string; setValue: (value: string) => void }; const Ctx = createContext<Ctx | null>(null); function useRadio() { const context = useContext(Ctx); if (!context) throw new Error("Use inside RadioGroup.Root"); return context; } function RadioGroupRoot({ value, defaultValue = "", onValueChange, children }: { value?: string; defaultValue?: string; onValueChange?: (value: string) => void; children: ReactNode }) { const [internal, setInternal] = useState(defaultValue); const active = value ?? internal; const setValue = (next: string) => { if (value === undefined) setInternal(next); onValueChange?.(next); }; return <Ctx.Provider value={{ value: active, setValue }}><div className="space-y-2">{children}</div></Ctx.Provider>; } function RadioGroupItem({ value, label, description }: { value: string; label: string; description?: string }) { const radio = useRadio(); const checked = radio.value === value; return ( <button type="button" role="radio" aria-checked={checked} onClick={() => radio.setValue(value)} className={"flex w-full items-start gap-3 rounded-lg border p-3 text-left " + (checked ? "border-primary bg-primary/5" : "border-slate-200")}> <span className={"mt-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full border " + (checked ? "border-primary" : "border-slate-400")}>{checked && <span className="h-2 w-2 rounded-full bg-primary" />}</span> <span> <span className="block text-sm font-medium text-slate-900 dark:text-white">{label}</span> {description && <span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">{description}</span>} </span> </button> ); } type RadioGroupCompound = typeof RadioGroupRoot & { Root: typeof RadioGroupRoot; Item: typeof RadioGroupItem }; export const RadioGroup = Object.assign(RadioGroupRoot, { Root: RadioGroupRoot, Item: RadioGroupItem }) as RadioGroupCompound;