Use Radio Group when one choice must be selected from a short visible set. Each item needs a unique value and its own label. Use Select when space is limited or the list is longer.
Live preview
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const densities = [
{
value: "comfortable",
label: "Comfortable",
detail: "More breathing room for forms and documentation.",
},
{
value: "balanced",
label: "Balanced",
detail: "A practical default for most product screens.",
},
{
value: "compact",
label: "Compact",
detail: "Higher information density for dashboards.",
},
];
export default function RadioGroupDemo() {
return (
<fieldset className="w-full max-w-md">
<legend className="mb-3 font-heading">Interface density</legend>
<RadioGroup defaultValue="balanced" className="grid gap-2">
{densities.map((density) => (
<div
key={density.value}
className="flex items-start gap-3 rounded-base border-2 border-border bg-secondary-background p-3 has-[[data-checked]]:bg-main has-[[data-checked]]:text-main-foreground"
>
<RadioGroupItem
value={density.value}
id={`density-${density.value}`}
className="mt-0.5"
/>
<Label
htmlFor={`density-${density.value}`}
className="grid cursor-pointer gap-1 leading-none"
>
<span className="font-heading">{density.label}</span>
<span className="text-sm font-base leading-5 opacity-80">{density.detail}</span>
</Label>
</div>
))}
</RadioGroup>
</fieldset>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/radio-group.jsonnpm install @base-ui/react@^1.8.0Create components/ui/radio-group.tsx:
"use client";
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
import { cn } from "@/lib/utils";
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props<string>) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
{...props}
className={(state) =>
cn("grid gap-2", typeof className === "function" ? className(state) : className)
}
/>
);
}
function RadioGroupItem({
className,
nativeButton,
render,
...props
}: RadioPrimitive.Root.Props<string>) {
const usesDefaultButton = render === undefined;
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
nativeButton={nativeButton ?? usesDefaultButton}
render={render ?? <button type="button" />}
{...props}
className={(state) =>
cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border-2 border-current text-inherit outline-none focus-visible:ring-1 focus-visible:ring-ring data-disabled:cursor-not-allowed data-disabled:opacity-50",
typeof className === "function" ? className(state) : className,
)
}
>
<RadioPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-current" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
);
}
export { RadioGroup, RadioGroupItem };
The preview and Usage example additionally use Label. Install it before copying either example:
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/label.json#Usage
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
export default function DeliveryPreference() {
return (
<fieldset className="grid gap-3">
<legend className="mb-3">Delivery frequency</legend>
<RadioGroup defaultValue="weekly" name="frequency">
<div className="flex items-center gap-2">
<RadioGroupItem value="daily" id="daily" />
<Label htmlFor="daily">Daily</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="weekly" id="weekly" />
<Label htmlFor="weekly">Weekly</Label>
</div>
</RadioGroup>
</fieldset>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
RadioGroup.value / onValueChange | Controlled selection | Uncontrolled |
RadioGroup.defaultValue | Initial choice | Unset |
RadioGroup.name | Form submission key | Unset |
RadioGroup.disabled / readOnly / required | Group constraints | false |
RadioGroup.form / inputRef | Native form association | — |
RadioGroupItem.value / id | Unique option and label target | Value required |
#Accessibility
Use a fieldset and legend or an accessible group name. Label each item; its value is not a visible description. Arrow keys change the focused selection. Do not simulate a mutually exclusive choice with independent checkboxes.