Interactive Pricing
A pricing section with morphing numbers and a premium toggle.
Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
npx -y vui-registry-cli-v1@latest add pricingSource Code
'use client'
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { cn } from '@/lib/utils'
interface PricingPlan {
id: string
name: string
price: {
monthly: number
yearly: number
}
description: string
features: string[]
popular?: boolean
}
const CheckIcon = ({ className }: { className?: string }) => (
<svg className={cn('w-3 h-3', className)} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={4}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)
export interface PricingProps {
plans?: PricingPlan[]
defaultBillingCycle?: 'monthly' | 'yearly'
className?: string
}
export function Pricing({ plans: plansProp, defaultBillingCycle = 'monthly', className }: PricingProps) {
const [isYearly, setIsYearly] = useState(defaultBillingCycle === 'yearly')
const plans: PricingPlan[] = plansProp ?? [
{
id: 'starter',
name: 'Starter',
price: { monthly: 15, yearly: 12 },
description: 'Perfect for side projects',
features: ['5 Projects', 'Basic Analytics', 'Community Support', '1GB Storage'],
},
{
id: 'pro',
name: 'Pro',
price: { monthly: 39, yearly: 32 },
description: 'For growing businesses',
features: ['Unlimited Projects', 'Advanced Analytics', 'Priority Support', 'Team Access', '10GB Storage', 'Custom Domain'],
popular: true,
},
{
id: 'enterprise',
name: 'Enterprise',
price: { monthly: 99, yearly: 89 },
description: 'For large scale apps',
features: ['Unlimited Everything', 'Custom SLA', 'Dedicated Manager', 'SSO', 'Unlimited Storage', 'On-premise Deployment'],
},
]
return (
<div className={cn('w-full max-w-6xl mx-auto px-4 py-12 flex flex-col items-center', className)}>
{/* Toggle Switch */}
<div className="flex items-center gap-4 mb-16 bg-muted/50 p-1.5 rounded-full border border-border backdrop-blur-sm">
<span
className={cn(
'px-3 py-1 text-sm font-medium transition-colors cursor-pointer',
!isYearly ? 'text-foreground' : 'text-muted-foreground'
)}
onClick={() => setIsYearly(false)}
>
Monthly
</span>
<button
onClick={() => setIsYearly(!isYearly)}
className="relative w-14 h-8 rounded-full bg-muted border border-border p-1 transition-colors hover:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/20"
>
<motion.div
className="w-6 h-6 rounded-full bg-primary shadow-sm"
animate={{ x: isYearly ? 24 : 0 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
</button>
<span
className={cn(
'px-3 py-1 text-sm font-medium transition-colors cursor-pointer flex items-center gap-2',
isYearly ? 'text-foreground' : 'text-muted-foreground'
)}
onClick={() => setIsYearly(true)}
>
Yearly{' '}
<span className="text-[10px] font-bold text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
-20%
</span>
</span>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 w-full items-start">
{plans.map((plan) => (
<PricingCard key={plan.id} plan={plan} isYearly={isYearly} />
))}
</div>
</div>
)
}
function PricingCard({ plan, isYearly }: { plan: PricingPlan; isYearly: boolean }) {
const isPopular = plan.popular
return (
<div
className={cn(
'relative flex flex-col p-6 rounded-3xl transition-all duration-300 group h-full',
isPopular
? 'bg-zinc-900 dark:bg-[#121212] shadow-2xl shadow-black/20 dark:shadow-black/50 z-10 md:scale-105 border border-zinc-800 dark:border-white/10'
: 'bg-card border border-border hover:border-border/80 shadow-sm hover:shadow-lg hover:-translate-y-1'
)}
>
<div
className={cn(
'absolute inset-0 rounded-3xl pointer-events-none transition-opacity duration-500 border-2',
isPopular ? 'border-white/5' : 'border-transparent group-hover:border-border/40'
)}
/>
{isPopular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full bg-foreground text-background text-[10px] font-bold shadow-lg tracking-widest uppercase border border-border/20">
Most Popular
</div>
)}
<div
className={cn(
'absolute inset-0 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none',
isPopular
? 'bg-[radial-gradient(600px_circle_at_var(--mouse-x,50%)_var(--mouse-y,0%),rgba(255,255,255,0.04),transparent_40%)]'
: 'bg-[radial-gradient(600px_circle_at_var(--mouse-x,50%)_var(--mouse-y,0%),hsl(var(--foreground)/0.04),transparent_40%)]'
)}
/>
<div className="mb-4 relative z-10">
<h3
className={cn(
'text-lg font-medium tracking-wide',
isPopular ? 'text-white' : 'text-foreground'
)}
>
{plan.name}
</h3>
<p
className={cn(
'text-xs mt-1 font-medium leading-relaxed',
isPopular ? 'text-white/50' : 'text-muted-foreground'
)}
>
{plan.description}
</p>
</div>
<div className="mb-6 flex items-baseline gap-1 relative z-10">
<span className={cn('text-xl font-bold', isPopular ? 'text-white/30' : 'text-muted-foreground')}>
$
</span>
<div className="h-10 relative flex items-center">
<AnimatePresence mode="popLayout">
<motion.span
key={isYearly ? 'year' : 'month'}
initial={{ y: 15, opacity: 0, filter: 'blur(4px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: -15, opacity: 0, filter: 'blur(4px)' }}
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
className={cn(
'text-4xl font-extrabold tracking-tight leading-none',
isPopular ? 'text-white' : 'text-foreground'
)}
>
{isYearly ? plan.price.yearly : plan.price.monthly}
</motion.span>
</AnimatePresence>
</div>
<span className={cn('text-sm font-medium ml-1', isPopular ? 'text-white/40' : 'text-muted-foreground')}>
/mo
</span>
</div>
<div className="flex-1 space-y-3 mb-8 relative z-10">
{plan.features.map((feature) => (
<div key={feature} className="flex items-center gap-3 text-sm group/item">
<div
className={cn(
'w-4 h-4 rounded-full flex items-center justify-center shrink-0 transition-all duration-300',
isPopular
? 'bg-white/10 text-white shadow-[0_0_10px_rgba(255,255,255,0.1)]'
: 'bg-muted text-foreground group-hover/item:bg-primary/10 group-hover/item:text-primary group-hover/item:scale-110'
)}
>
<CheckIcon className={isPopular ? 'text-white' : undefined} />
</div>
<span
className={cn(
'transition-colors duration-300 text-xs',
isPopular
? 'text-white/60 group-hover/item:text-white/90'
: 'text-muted-foreground group-hover/item:text-foreground'
)}
>
{feature}
</span>
</div>
))}
</div>
<button
className={cn(
'w-full py-3 rounded-xl font-semibold text-xs transition-all duration-300 relative overflow-hidden z-10 border tracking-wide uppercase',
isPopular
? 'bg-white text-zinc-900 border-transparent hover:shadow-[0_0_20px_rgba(255,255,255,0.2)] hover:scale-[1.02]'
: 'bg-transparent border-border text-foreground hover:bg-muted hover:border-border/80'
)}
>
Get Started
</button>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| plans | Array<{ id: string; name: string; price: { monthly: number; yearly: number }; description: string; features: string[]; popular?: boolean }> | Built-in plans | Custom pricing plans to display. |
| defaultBillingCycle | 'monthly' | 'yearly' | 'monthly' | Initial billing cycle. |
| className | string | undefined | Additional CSS classes. |
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.