Radio Group
A set of checkable buttons where only one can be selected at a time.
Installation
$
Usage
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"Examples
Default
One choice from a short list. Arrow keys move between the options, so the whole group is a single tab stop rather than one per item.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValueRadioGroup | string | - | The value of the item that is checked initially, for uncontrolled usage. |
| valueRadioGroup, RadioGroupItem | string | - | On <RadioGroup />, the controlled checked value (use with onValueChange). On <RadioGroupItem />, the value this option submits when checked (required). |
| onValueChangeRadioGroup | (value: string) => void | - | Called with the newly checked item's value. |
| nameRadioGroup | string | - | The name submitted with form data. |
| disabledRadioGroup, RadioGroupItem | boolean | false | Disables every item, or a single option on <RadioGroupItem />. |
| requiredRadioGroup | boolean | false | Requires a selection before the owning form can be submitted. |
| orientationRadioGroup | "horizontal" | "vertical" | - | Axis used for arrow-key navigation between items. |
| loopRadioGroup | boolean | true | When true, arrow-key navigation wraps from the last item back to the first. |
"use client"
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { cn } from "@/registry/lib/utils"
// @use-when choosing exactly one option from a small visible set.
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive>) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioPrimitive.Root>) {
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
className={cn(
"peer group/radio tap-target border-border dark:bg-neutral-800 data-[checked]:bg-accent data-[checked]:text-accent-foreground dark:data-[checked]:bg-accent data-[checked]:border-accent focus-visible:border-accent focus-visible:ring-ring-accent aria-invalid:ring-ring-destructive aria-invalid:border-destructive size-4 shrink-0 rounded-full border shadow-xs transition-[transform,color,background-color,border-color,box-shadow] motion-reduce:transition-[color,background-color,border-color,box-shadow] duration-150 ease-out active:scale-[0.98] outline-none focus-visible:ring-1 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70",
className
)}
{...props}
>
{/* Kept mounted (`keepMounted`) so the dot never unmounts; its show/hide
is a CSS transition on transform+opacity keyed off the root's checked
state, so enter and exit run the same 150ms path and moving fast
between radios interrupts cleanly instead of leaving the outgoing dot
to linger while Base UI waits to unmount it. Under reduced motion the
scale is pinned and only the fade runs. */}
<RadioPrimitive.Indicator
keepMounted
data-slot="radio-group-indicator"
className="grid size-full place-content-center text-current scale-50 opacity-0 transition-[transform,opacity] duration-150 ease-out group-data-[checked]/radio:scale-100 group-data-[checked]/radio:opacity-100 motion-reduce:scale-100 motion-reduce:transition-[opacity]"
>
<span className="size-1.5 rounded-full bg-current" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
)
}
export { RadioGroup, RadioGroupItem }