A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
Installation#
pnpm dlx shadcn@latest add https://ui.tyap.me/r/styles/base/radio-group.jsonnpx shadcn@latest add https://ui.tyap.me/r/styles/base/radio-group.jsonyarn dlx shadcn@latest add https://ui.tyap.me/r/styles/base/radio-group.jsonbunx --bun shadcn@latest add https://ui.tyap.me/r/styles/base/radio-group.json
Install the following dependencies:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Copy and paste the following code into your project.
"use client"
import * as React from "react"
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { useShakeOnInvalid } from "@/hooks/use-shake-on-invalid"
import { cn } from "@/lib/utils"
const RADIO_ANIMATION_CSS = `
.cn-radio-group-item {
transition:
border-color 220ms cubic-bezier(0.22, 1, 0.36, 1),
background-color 220ms cubic-bezier(0.22, 1, 0.36, 1),
transform 130ms cubic-bezier(0.22, 1, 0.36, 1);
cursor: pointer;
}
.cn-radio-group-item:not(:disabled):not([data-disabled]):active {
transform: scale(0.89);
transition-duration: 70ms;
}
.cn-radio-group-item::before {
content: '';
position: absolute;
inset: 0;
margin: auto;
width: 5px;
height: 5px;
border-radius: 9999px;
background: var(--foreground);
opacity: 0.18;
transition: opacity 160ms ease, scale 160ms ease;
}
.cn-radio-group-item[data-checked]::before {
opacity: 0;
scale: 0;
}
.cn-radio-group-item[data-disabled]::before,
.cn-radio-group-item:disabled::before {
opacity: 0.08;
}
.cn-radio-group-indicator-icon {
animation: cn-radio-dot-in 160ms ease-out both;
}
@keyframes cn-radio-dot-in {
from { opacity: 0; scale: 0.4; }
to { opacity: 1; scale: 1; }
}
@media (prefers-reduced-motion: reduce) {
.cn-radio-group-item,
.cn-radio-group-item::before { transition: none !important; }
.cn-radio-group-indicator-icon { animation: none !important; }
}
`
type RadioGroupCtxValue = {
groupValue: string | undefined
clearValue: () => void
}
type RadioGroupValueChange = NonNullable<
RadioGroupPrimitive.Props["onValueChange"]
>
type RadioGroupValue = Parameters<RadioGroupValueChange>[0]
type RadioGroupChangeDetails = Parameters<RadioGroupValueChange>[1]
const RadioGroupCtx = React.createContext<RadioGroupCtxValue>({
groupValue: undefined,
clearValue: () => {},
})
function RadioGroup({
value: valueProp,
defaultValue,
onValueChange,
className,
...props
}: RadioGroupPrimitive.Props) {
const isControlled = valueProp !== undefined
const [internalValue, setInternalValue] = React.useState<string | undefined>(
defaultValue
)
const groupValue = isControlled ? valueProp : internalValue
const handleValueChange = (
val: RadioGroupValue,
eventDetails: RadioGroupChangeDetails
) => {
if (!isControlled) setInternalValue(val)
onValueChange?.(val, eventDetails)
}
const clearValue = React.useCallback(() => {
if (!isControlled) setInternalValue(undefined)
}, [isControlled])
return (
<RadioGroupCtx.Provider value={{ groupValue, clearValue }}>
<style precedence="component" href="cn-radio-group-transitions">
{RADIO_ANIMATION_CSS}
</style>
<RadioGroupPrimitive
data-slot="radio-group"
value={groupValue ?? ""}
onValueChange={handleValueChange}
className={cn("w-full", className)}
{...props}
/>
</RadioGroupCtx.Provider>
)
}
function RadioGroupItem({
value,
onClick,
className,
...props
}: RadioPrimitive.Root.Props) {
const ref = React.useRef<HTMLSpanElement>(null)
const { groupValue, clearValue } = React.useContext(RadioGroupCtx)
useShakeOnInvalid(ref)
const handleClick: RadioPrimitive.Root.Props["onClick"] = (e) => {
if (value !== undefined && value === groupValue) {
clearValue()
e.preventDefault()
}
onClick?.(e)
}
return (
<RadioPrimitive.Root
ref={ref}
value={value}
onClick={handleClick}
data-slot="radio-group-item"
className={cn(
"t-input group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioPrimitive.Indicator
data-slot="radio-group-indicator"
className=""
>
<span className="" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
)
}
export { RadioGroup, RadioGroupItem }
Update the import paths to match your project setup.
Usage#
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"<RadioGroup defaultValue="option-one">
<div className="flex items-center gap-3">
<RadioGroupItem value="option-one" id="option-one" />
<Label htmlFor="option-one">Option One</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="option-two" id="option-two" />
<Label htmlFor="option-two">Option Two</Label>
</div>
</RadioGroup>Composition#
Use the following composition to build a RadioGroup:
RadioGroup
├── RadioGroupItem
└── RadioGroupItemExamples#
Description#
Radio group items with a description using the Field component.
Standard spacing for most use cases.
More space between elements.
Minimal spacing for dense layouts.
import {
Field,
FieldContent,Choice Card#
Use FieldLabel to wrap the entire Field for a clickable card-style selection.
import {
Field,
FieldContent,Fieldset#
Use FieldSet and FieldLegend to group radio items with a label and description.
import {
Field,
FieldDescription,Disabled#
Use the disabled prop on RadioGroup to disable all items.
import { Field, FieldLabel } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
Invalid#
Use aria-invalid on RadioGroupItem and data-invalid on Field to show validation errors.
import {
Field,
FieldDescription,RTL#
To enable RTL support in shadcn/ui, see the RTL configuration guide.
تباعد قياسي لمعظم حالات الاستخدام.
مساحة أكبر بين العناصر.
تباعد أدنى للتخطيطات الكثيفة.
"use client"
import * as React from "react"API Reference#
See the Base UI Radio Group documentation.