FluxUI Pro is live - modern UI, powerful animations, zero hassle.
Components
Radio

Radio

Ultra-modern radio button component with custom animations, multiple variants, and enhanced user interaction for form controls.

Radio Button Groups

Choose Size

Perfect for personal use

Great for small teams

Ideal for growing businesses

Selected: None

Choose Plan

$9/month - Core features

$29/month - Advanced features

$99/month - All features

Selected: None

Radio Variants

Default Variant

Filled Variant

Outlined Variant

Radio Sizes

Radio States

Normal State

Error State

Please select an option

Disabled State

Interactive Form Demo

Complete Form Example

Payment Method

Visa, Mastercard, Amex

Fast and secure payment

Direct bank transfer

Theme Preference

Clean and bright interface

Easy on the eyes

Follows system preference

Keyboard Navigation

Try these keyboard shortcuts:

  • Tab: Move focus between radio groups
  • Arrow Keys: Navigate within a group
  • Space/Enter: Select focused radio
  • Click: Select radio directly

Test Navigation Group

Installation

1

Install the packages

npm i motion clsx tailwind-merge lucide-react
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

radio.tsx
"use client"; import { motion, AnimatePresence } from "motion/react"; import React, { useState, useId } from "react"; import { cn } from "@/lib/utils"; import { Check, AlertCircle } from "lucide-react"; interface RadioProps { checked?: boolean; value?: string | number; name?: string; onChange?: (value: string | number) => void; label?: string; description?: string; disabled?: boolean; required?: boolean; error?: string; size?: "sm" | "md" | "lg"; variant?: "default" | "filled" | "outlined"; className?: string; } const Radio: React.FC<RadioProps> = ({ checked = false, value, name, onChange, label, description, disabled = false, required = false, error, size = "md", variant = "default", className, }) => { const [isFocused, setIsFocused] = useState(false); const radioId = useId(); const hasError = !!error; const sizeClasses = { sm: "w-4 h-4", md: "w-5 h-5", lg: "w-6 h-6", }; const dotSizeClasses = { sm: "w-2 h-2", md: "w-3 h-3", lg: "w-4 h-4", }; const textSizeClasses = { sm: "text-sm", md: "text-base", lg: "text-lg", }; const variantClasses = { default: "border-gray-300 bg-white hover:border-gray-400 focus:border-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:hover:border-gray-500", filled: "border-transparent bg-gray-100 hover:bg-gray-200 focus:bg-white focus:border-blue-500 dark:bg-gray-700 dark:hover:bg-gray-600", outlined: "border-2 border-gray-300 bg-transparent hover:border-gray-400 focus:border-blue-500 dark:border-gray-600", }; const checkedClasses = { default: "bg-blue-500 border-blue-500 dark:bg-blue-600 dark:border-blue-600", filled: "bg-blue-500 border-blue-500 dark:bg-blue-600 dark:border-blue-600", outlined: "bg-blue-500 border-blue-500 dark:bg-blue-600 dark:border-blue-600", }; const handleClick = () => { if (!disabled && value !== undefined) { onChange?.(value); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } }; return ( <div className={cn("relative", className)}> <div className="flex items-start space-x-3"> {/* Radio Input */} <div className="relative flex-shrink-0"> <motion.input id={radioId} type="radio" name={name} value={value} checked={checked} onChange={() => {}} // Handled by click disabled={disabled} className="sr-only" /> {/* Custom Radio */} <motion.div className={cn( "relative rounded-full border-2 cursor-pointer transition-all duration-200 flex items-center justify-center", sizeClasses[size], checked ? checkedClasses[variant] : variantClasses[variant], disabled && "opacity-50 cursor-not-allowed", hasError && !checked && "border-red-500", isFocused && "ring-2 ring-blue-500/20" )} onClick={handleClick} onKeyDown={handleKeyDown} tabIndex={disabled ? -1 : 0} role="radio" aria-checked={checked} aria-labelledby={label ? radioId : undefined} animate={{ scale: isFocused ? 1.05 : 1, boxShadow: isFocused ? "0 0 0 3px rgba(59, 130, 246, 0.1)" : "0 0 0 0px rgba(0, 0, 0, 0)", }} transition={{ duration: 0.2 }} whileHover={!disabled ? { scale: 1.05 } : {}} whileTap={!disabled ? { scale: 0.95 } : {}} > {/* Background Animation */} <AnimatePresence> {checked && ( <motion.div className="absolute inset-0 bg-current rounded-full" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.2 }} /> )} </AnimatePresence> {/* Dot */} <AnimatePresence> {checked && ( <motion.div className={cn("bg-white rounded-full", dotSizeClasses[size])} initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.2, delay: 0.1 }} /> )} </AnimatePresence> {/* Ripple Effect */} <AnimatePresence> {isFocused && ( <motion.div className="absolute inset-0 bg-white/30 rounded-full" initial={{ scale: 0, opacity: 1 }} animate={{ scale: 2, opacity: 0 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.4 }} /> )} </AnimatePresence> </motion.div> {/* Focus Ring */} <AnimatePresence> {isFocused && ( <motion.div className="absolute inset-0 rounded-full border-2 border-blue-500" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.2 }} /> )} </AnimatePresence> </div> {/* Label and Description */} <div className="flex-1 min-w-0"> {label && ( <motion.label htmlFor={radioId} className={cn( "block font-medium cursor-pointer transition-colors duration-200", textSizeClasses[size], hasError ? "text-red-600 dark:text-red-400" : disabled ? "text-gray-400 dark:text-gray-500" : "text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-200" )} onClick={handleClick} animate={{ x: isFocused ? 2 : 0, }} transition={{ duration: 0.2 }} > {label} {required && <span className="text-red-500 ml-1">*</span>} </motion.label> )} {description && ( <motion.p className={cn( "mt-1 text-sm transition-colors duration-200", disabled ? "text-gray-400 dark:text-gray-500" : "text-gray-600 dark:text-gray-400" )} animate={{ x: isFocused ? 2 : 0, }} transition={{ duration: 0.2, delay: 0.05 }} > {description} </motion.p> )} {/* Error Message */} <AnimatePresence> {hasError && ( <motion.div initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: "auto" }} exit={{ opacity: 0, y: -10, height: 0 }} transition={{ duration: 0.2 }} className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1" > <AlertCircle className="w-4 h-4 flex-shrink-0" /> {error} </motion.div> )} </AnimatePresence> </div> </div> </div> ); }; export default Radio;
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
checkedbooleanfalseWhether the radio button is checked.
valuestring | numberundefinedThe value of the radio button.
namestringundefinedThe name attribute for grouping radio buttons.
onChange(value: string | number) => void() => {}Callback function when radio button is selected.
labelstringundefinedLabel text for the radio button.
descriptionstringundefinedDescription text below the label.
disabledbooleanfalseDisable the radio button interaction.
requiredbooleanfalseMark radio button as required.
errorstringundefinedError message to display.
size'sm' | 'md' | 'lg''md'Size of the radio button.
variant'default' | 'filled' | 'outlined''default'Visual style variant.