Radio Group Collection
Premium radio group variants featuring Modern, Card-based, Neon, and Animated interaction models.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add radio-stackSource Code
'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import RadioBasic from './radio-basic'
import RadioPill from './radio-pill'
import RadioGlow from './radio-glow'
import RadioCapsule from './radio-capsule'
import RadioSegmented from './radio-segmented'
import RadioToggle from './radio-toggle'
export default function RadioStack() {
const [copied, setCopied] = useState<string | null>(null)
function copy(name: string, cmd: string) {
navigator.clipboard.writeText(cmd)
setCopied(name)
setTimeout(() => setCopied(null), 1500)
}
const variants = [
{ name: 'Basic', Component: RadioBasic, cmd: 'npx vui-registry-cli-v1 add radio-basic' },
{ name: 'Pill', Component: RadioPill, cmd: 'npx vui-registry-cli-v1 add radio-pill' },
{ name: 'Glow', Component: RadioGlow, cmd: 'npx vui-registry-cli-v1 add radio-glow' },
{ name: 'Capsule', Component: RadioCapsule, cmd: 'npx vui-registry-cli-v1 add radio-capsule' },
{ name: 'Segmented', Component: RadioSegmented, cmd: 'npx vui-registry-cli-v1 add radio-segmented' },
{ name: 'Toggle', Component: RadioToggle, cmd: 'npx vui-registry-cli-v1 add radio-toggle' },
]
return (
<div className="w-full grid gap-6 md:grid-cols-2 p-4">
{variants.map((v) => (
<div key={v.name} className="rounded-2xl p-[2px] border border-border bg-card/50 backdrop-blur-sm overflow-hidden">
<div className="rounded-xl border border-border bg-background/60">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-muted/10 relative">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">{v.name}</div>
<button
onClick={() => copy(v.name, v.cmd)}
className="text-[10px] font-mono border-b border-border absolute top-2 right-3"
>
<AnimatePresence mode="wait">
{copied === v.name ? (
<motion.span key="copied" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="text-emerald-500">
Copied
</motion.span>
) : (
<motion.span key="copy" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
CLI
</motion.span>
)}
</AnimatePresence>
</button>
</div>
<div className="p-6 min-h-[160px] grid place-items-center">
<v.Component />
</div>
</div>
</div>
))}
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioBasic() {
const [value, setValue] = useState('a')
const options = ['a', 'b', 'c']
return (
<div className="flex items-center gap-3 p-3 rounded-2xl border border-border bg-background/40 backdrop-blur-sm">
{options.map((opt, i) => (
<button
key={opt}
onClick={() => setValue(opt)}
className="relative px-3.5 py-2 rounded-xl text-sm font-medium"
>
<motion.span
animate={{ opacity: value === opt ? 1 : 0.6, scale: value === opt ? 1.02 : 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 24 }}
className={value === opt ? 'text-primary' : 'text-muted-foreground'}
>
{opt.toUpperCase()}
</motion.span>
{value === opt && (
<motion.div
layoutId="radio-basic"
className="absolute inset-0 rounded-xl bg-primary/10 border border-primary/20"
transition={{ type: 'spring', stiffness: 350, damping: 26 }}
/>
)}
</button>
))}
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioCapsule() {
const [value, setValue] = useState('xs')
const options = ['xs', 'sm', 'md', 'lg']
const w = 52
return (
<div className="relative px-2 py-2 rounded-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800">
<div className="relative flex items-center">
<motion.div
layout
className="absolute top-0 bottom-0 rounded-full bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 shadow-sm"
style={{ width: w, left: options.indexOf(value) * w }}
transition={{ type: 'spring', stiffness: 380, damping: 28 }}
/>
{options.map((opt) => (
<button
key={opt}
onClick={() => setValue(opt)}
className="relative w-[52px] h-10 grid place-items-center rounded-full z-10 text-xs font-semibold"
>
<span className={value === opt ? 'text-black dark:text-white' : 'text-zinc-500'}>
{opt.toUpperCase()}
</span>
</button>
))}
</div>
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioGlow() {
const [value, setValue] = useState('one')
const options = ['one', 'two', 'three', 'four']
return (
<div className="relative flex items-center gap-2 p-3 rounded-2xl bg-black border border-neutral-800">
{options.map((opt) => (
<button
key={opt}
onClick={() => setValue(opt)}
className="relative w-16 h-10 grid place-items-center rounded-xl z-10 text-xs font-semibold"
>
<span className={value === opt ? 'text-white' : 'text-neutral-400'}>{opt.toUpperCase()}</span>
{value === opt && (
<motion.div
layoutId="radio-glow"
className="absolute inset-0 rounded-xl bg-indigo-500/15 border border-indigo-500/40"
style={{ boxShadow: '0 0 18px 2px rgba(99,102,241,0.35), inset 0 0 10px rgba(99,102,241,0.2)' }}
transition={{ type: 'spring', stiffness: 320, damping: 24 }}
/>
)}
</button>
))}
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioPill() {
const [value, setValue] = useState('apple')
const options = ['apple', 'google', 'github']
return (
<div className="relative flex items-center gap-2 p-2 rounded-full bg-muted/30 border border-border/50">
<motion.div
layout
className="absolute top-1/2 -translate-y-1/2 rounded-full bg-background shadow-sm border border-border"
style={{ width: 90, height: 38, left: options.indexOf(value) * 96 + 2 }}
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
/>
{options.map((opt) => (
<button
key={opt}
onClick={() => setValue(opt)}
className="relative w-[90px] h-[38px] rounded-full text-xs font-semibold z-10"
>
<motion.span
animate={{ scale: value === opt ? 1.05 : 0.95, opacity: value === opt ? 1 : 0.7 }}
transition={{ type: 'spring', stiffness: 450, damping: 24 }}
className={value === opt ? 'text-foreground' : 'text-muted-foreground'}
>
{opt.toUpperCase()}
</motion.span>
</button>
))}
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioSegmented() {
const [value, setValue] = useState('weekly')
const options = ['daily', 'weekly', 'monthly']
return (
<div className="relative flex items-center gap-1 p-1 rounded-2xl bg-background/50 border border-border/60 shadow-inner">
{options.map((opt) => (
<button
key={opt}
onClick={() => setValue(opt)}
className="relative px-4 h-10 grid place-items-center rounded-xl z-10 text-xs font-semibold"
>
<motion.span
animate={{ scale: value === opt ? 1.05 : 1, opacity: value === opt ? 1 : 0.7 }}
transition={{ type: 'spring', stiffness: 420, damping: 26 }}
className={value === opt ? 'text-foreground' : 'text-muted-foreground'}
>
{opt.toUpperCase()}
</motion.span>
{value === opt && (
<motion.div
layoutId="radio-segment"
className="absolute inset-0 rounded-xl bg-primary/8 border border-primary/20"
transition={{ type: 'spring', stiffness: 360, damping: 24 }}
/>
)}
</button>
))}
</div>
)
}
Source Code
'use client'
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function RadioToggle() {
const [value, setValue] = useState<'on' | 'off'>('off')
return (
<div className="relative w-[220px] p-2 rounded-2xl bg-background/50 border border-border/60">
<div className="relative h-10 flex items-center justify-between px-2">
<motion.div
layout
className="absolute top-0 bottom-0 rounded-xl bg-foreground/5 border border-border/50"
style={{ width: 100, left: value === 'on' ? 110 : 10 }}
transition={{ type: 'spring', stiffness: 380, damping: 28 }}
/>
<button
onClick={() => setValue('off')}
className="relative w-[100px] h-10 grid place-items-center rounded-xl z-10 text-xs font-semibold"
>
<motion.span
animate={{ scale: value === 'off' ? 1.06 : 1, opacity: value === 'off' ? 1 : 0.7 }}
transition={{ type: 'spring', stiffness: 420, damping: 26 }}
className={value === 'off' ? 'text-foreground' : 'text-muted-foreground'}
>
OFF
</motion.span>
</button>
<button
onClick={() => setValue('on')}
className="relative w-[100px] h-10 grid place-items-center rounded-xl z-10 text-xs font-semibold"
>
<motion.span
animate={{ scale: value === 'on' ? 1.06 : 1, opacity: value === 'on' ? 1 : 0.7 }}
transition={{ type: 'spring', stiffness: 420, damping: 26 }}
className={value === 'on' ? 'text-foreground' : 'text-muted-foreground'}
>
ON
</motion.span>
</button>
</div>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| options | RadioOption[] | [] | Array of radio options with value, label, and optional icon. |
| value | string | undefined | The currently selected value. |
| onChange | (value: string) => void | undefined | Callback function when a selection is made. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

