UI/UI
Radio
A customizable radio group component with smooth animations and flexible layouts.
<RadioGroup defaultValue="option1">
<RadioItem value="option1" label="Option 1" />
<RadioItem value="option2" label="Option 2" />
<RadioItem value="option3" label="Option 3" />
</RadioGroup>
Installation
Install the required dependencies:
npm install @radix-ui/react-radio-group motion
pnpm add @radix-ui/react-radio-group motion
yarn add @radix-ui/react-radio-group motion
bun add @radix-ui/react-radio-group motion
Copy and paste the following code into your project:
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "motion/react";
const radioGroupVariants = cva("grid gap-2", {
variants: {
orientation: {
vertical: "grid-cols-1",
horizontal: "grid-flow-col auto-cols-max",
},
},
defaultVariants: {
orientation: "vertical",
},
});
const radioVariants = cva(
"aspect-square rounded-full border border-[hsl(var(--hu-border))] text-[hsl(var(--hu-primary))] focus:outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--hu-ring))] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-[hsl(var(--hu-primary))]",
{
variants: {
size: {
sm: "h-3 w-3",
default: "h-4 w-4",
lg: "h-5 w-5",
},
},
defaultVariants: {
size: "default",
},
}
);
interface RadioGroupProps
extends Omit<
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>,
"orientation"
>,
VariantProps<typeof radioGroupVariants> {
label?: string;
description?: string;
error?: string;
}
interface RadioItemProps
extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
VariantProps<typeof radioVariants> {
label?: string;
description?: string;
}
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
RadioGroupProps
>(
(
{ className, orientation, label, description, error, id, ...props },
ref
) => {
const groupId = id || React.useId();
return (
<div className="flex flex-col gap-4">
{(label || description) && (
<div className="grid gap-1.5">
{label && (
<label
htmlFor={groupId}
className="text-sm font-medium leading-none"
>
{label}
</label>
)}
{description && (
<p className="text-xs text-[hsl(var(--hu-muted-foreground))]">
{description}
</p>
)}
</div>
)}
<RadioGroupPrimitive.Root
ref={ref}
id={groupId}
className={cn(radioGroupVariants({ orientation }), className)}
{...props}
/>
{error && (
<p className="text-xs text-[hsl(var(--hu-destructive))]">{error}</p>
)}
</div>
);
}
);
RadioGroup.displayName = "RadioGroup";
const RadioItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
RadioItemProps
>(({ className, size, label, description, id, ...props }, ref) => {
const itemId = id || React.useId();
const dotSize = size === "sm" ? 5 : size === "lg" ? 8 : 6;
return (
<div className="flex flex-col gap-1">
<div className="flex items-start gap-2">
<RadioGroupPrimitive.Item
ref={ref}
id={itemId}
className={cn(radioVariants({ size }), className)}
{...props}
>
<RadioGroupPrimitive.Indicator asChild>
<div className="flex items-center justify-center w-full h-full">
<AnimatePresence>
<motion.div
key="dot"
className="rounded-full bg-[hsl(var(--hu-primary))]"
style={{
width: dotSize,
height: dotSize,
}}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{
duration: 0.2,
ease: "easeInOut",
}}
/>
</AnimatePresence>
</div>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
{(label || description) && (
<div className="grid gap-1.5 leading-none">
{label && (
<label
htmlFor={itemId}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{label}
</label>
)}
{description && (
<p className="text-xs text-[hsl(var(--hu-muted-foreground))] peer-disabled:opacity-70">
{description}
</p>
)}
</div>
)}
</div>
</div>
);
});
RadioItem.displayName = "RadioItem";
export {
RadioGroup,
RadioItem,
radioGroupVariants,
radioVariants,
type RadioGroupProps,
type RadioItemProps,
};
Update your utils.ts
file if you haven't already:
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Coming soon...
Usage
import { RadioGroup, RadioItem } from "@/components/ui/Radio";
<RadioGroup defaultValue="option1">
<RadioItem value="option1" label="Option 1" />
<RadioItem value="option2" label="Option 2" />
<RadioItem value="option3" label="Option 3" />
</RadioGroup>
Basic Radio Group
<RadioGroup defaultValue="option1">
<RadioItem value="option1" label="Option 1" />
<RadioItem value="option2" label="Option 2" />
<RadioItem value="option3" label="Option 3" />
</RadioGroup>
With Description
Choose the option that best fits your needs
A relaxed and easy-going approach
Minimal space with essential features
Plenty of room with all amenities
<RadioGroup
defaultValue="comfortable"
label="Select your comfort level"
description="Choose the option that best fits your needs"
>
<RadioItem
value="comfortable"
label="Comfortable"
description="A relaxed and easy-going approach"
/>
<RadioItem
value="compact"
label="Compact"
description="Minimal space with essential features"
/>
<RadioItem
value="spacious"
label="Spacious"
description="Plenty of room with all amenities"
/>
</RadioGroup>
Sizes
Small
Default
Large
<div className="space-y-6">
<div>
<h4 className="text-sm font-medium mb-3">Small</h4>
<RadioGroup defaultValue="small1">
<RadioItem value="small1" label="Small option 1" size="sm" />
<RadioItem value="small2" label="Small option 2" size="sm" />
</RadioGroup>
</div>
<div>
<h4 className="text-sm font-medium mb-3">Default</h4>
<RadioGroup defaultValue="default1">
<RadioItem value="default1" label="Default option 1" />
<RadioItem value="default2" label="Default option 2" />
</RadioGroup>
</div>
<div>
<h4 className="text-sm font-medium mb-3">Large</h4>
<RadioGroup defaultValue="large1">
<RadioItem value="large1" label="Large option 1" size="lg" />
<RadioItem value="large2" label="Large option 2" size="lg" />
</RadioGroup>
</div>
</div>
Horizontal Layout
<RadioGroup
defaultValue="yes"
orientation="horizontal"
label="Do you agree?"
>
<RadioItem value="yes" label="Yes" />
<RadioItem value="no" label="No" />
<RadioItem value="maybe" label="Maybe" />
</RadioGroup>
Disabled State
<RadioGroup defaultValue="option1" disabled>
<RadioItem value="option1" label="Disabled option 1" />
<RadioItem value="option2" label="Disabled option 2" />
<RadioItem value="option3" label="Disabled option 3" />
</RadioGroup>
With Error
Basic features
Advanced features
All features
Please select a plan to continue
<RadioGroup label="Select a plan" error="Please select a plan to continue">
<RadioItem value="free" label="Free Plan" description="Basic features" />
<RadioItem value="pro" label="Pro Plan" description="Advanced features" />
<RadioItem
value="enterprise"
label="Enterprise Plan"
description="All features"
/>
</RadioGroup>
Controlled
Currently selected: option2
<div className="space-y-4">
<RadioGroup
value={value}
onValueChange={setValue}
label="Controlled Radio Group"
description={`Currently selected: ${value}`}
>
<RadioItem value="option1" label="Option 1" />
<RadioItem value="option2" label="Option 2" />
<RadioItem value="option3" label="Option 3" />
</RadioGroup>
<div className="flex gap-2">
<button
onClick={() => setValue("option1")}
className="px-3 py-1 text-xs bg-gray-100 rounded"
>
Select Option 1
</button>
<button
onClick={() => setValue("option2")}
className="px-3 py-1 text-xs bg-gray-100 rounded"
>
Select Option 2
</button>
<button
onClick={() => setValue("option3")}
className="px-3 py-1 text-xs bg-gray-100 rounded"
>
Select Option 3
</button>
</div>
</div>
); ```
</Tab>
</Tabs>
### Payment Method Example
<Tabs items={["Preview", "Code"]}>
<Tab value="Preview">
<PreviewContainer>
<PaymentMethodRadio />
</PreviewContainer>
</Tab>
<Tab value="Code">
```tsx
<RadioGroup
defaultValue="card"
label="Payment Method"
description="Select your preferred payment method"
>
<RadioItem
value="card"
label="Credit Card"
description="Pay with Visa, Mastercard, or American Express"
/>
<RadioItem
value="paypal"
label="PayPal"
description="Pay with your PayPal account"
/>
<RadioItem
value="bank"
label="Bank Transfer"
description="Direct transfer from your bank account"
/>
<RadioItem
value="crypto"
label="Cryptocurrency"
description="Pay with Bitcoin, Ethereum, or other crypto"
/>
</RadioGroup>
API Reference
RadioGroup
Prop | Type | Default |
---|---|---|
disabled? | boolean | - |
onValueChange? | (value: string) => void | - |
value? | string | - |
defaultValue? | string | - |
error? | string | - |
description? | string | - |
label? | string | - |
orientation? | "vertical" | "horizontal" | "vertical" |
RadioItem
Prop | Type | Default |
---|---|---|
disabled? | boolean | - |
description? | string | - |
label? | string | - |
size? | "sm" | "default" | "lg" | "default" |
value | string | - |
Edit on GitHub
Last updated on