Dynamic Checkout Flow
A seamless, token-aware multi-step checkout flow with animated transitions, an interactive card, and success celebration.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add dynamic-checkoutSource Code
'use client'
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
CreditCard,
MapPin,
Check,
ChevronRight,
ChevronLeft,
ShieldCheck,
Loader2,
Package,
Truck,
Sparkles,
Wifi,
ScanLine
} from 'lucide-react'
import { cn } from '@/lib/utils'
// @ts-ignore
import confetti from 'canvas-confetti'
type Step = 'shipping' | 'payment' | 'review' | 'success'
interface FormData {
name: string
address: string
city: string
zip: string
cardNumber: string
expiry: string
cvv: string
}
export default function DynamicCheckout() {
const [step, setStep] = useState<Step>('shipping')
const [direction, setDirection] = useState(1)
const [loading, setLoading] = useState(false)
const [focusedField, setFocusedField] = useState<string | null>(null)
const [formData, setFormData] = useState<FormData>({
name: '',
address: '',
city: '',
zip: '',
cardNumber: '',
expiry: '',
cvv: ''
})
const handleNext = () => {
setDirection(1)
if (step === 'shipping') setStep('payment')
else if (step === 'payment') setStep('review')
else if (step === 'review') handleSubmit()
}
const handleBack = () => {
setDirection(-1)
if (step === 'payment') setStep('shipping')
else if (step === 'review') setStep('payment')
}
const handleSubmit = () => {
setLoading(true)
setTimeout(() => {
setLoading(false)
setStep('success')
confetti({
particleCount: 150,
spread: 80,
origin: { y: 0.6 },
colors: ['#FFD700', '#FFA500', '#FF4500'] // Gold/Premium colors
})
}, 1500)
}
const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 20 : -20,
opacity: 0,
scale: 0.98,
filter: "blur(4px)"
}),
center: {
x: 0,
opacity: 1,
scale: 1,
filter: "blur(0px)"
},
exit: (direction: number) => ({
x: direction > 0 ? -20 : 20,
opacity: 0,
scale: 0.98,
filter: "blur(4px)"
})
}
return (
<div className="min-h-[600px] w-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-950 p-4 font-sans relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:24px_24px]"></div>
<motion.div
layout
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
className="bg-white/80 dark:bg-neutral-900/80 backdrop-blur-xl rounded-2xl shadow-2xl shadow-black/10 dark:shadow-black/50 border border-neutral-200/50 dark:border-white/10 w-full max-w-[360px] overflow-hidden relative z-10"
>
{/* Header */}
<div className="px-5 pt-5 pb-3 flex items-center justify-between border-b border-neutral-100 dark:border-white/5">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-neutral-900 to-neutral-700 dark:from-white dark:to-neutral-300 flex items-center justify-center shadow-lg shadow-neutral-500/20 group cursor-pointer overflow-hidden relative">
<motion.div
className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/40 to-transparent z-10"
animate={{ x: ['-100%', '200%'] }}
transition={{ duration: 1.5, repeat: Infinity, repeatDelay: 3 }}
/>
{/* Removed icon for a cleaner Velocity Pay logo */}
</div>
<div>
<span className="font-bold text-sm tracking-tight text-neutral-900 dark:text-white block leading-none">Velocity Pay</span>
<span className="text-[9px] text-neutral-500 font-medium tracking-wider uppercase">Secure Checkout</span>
</div>
</div>
{step !== 'success' && (
<div className="flex gap-1">
{['shipping', 'payment', 'review'].map((s, i) => {
const currentIndex = ['shipping', 'payment', 'review'].indexOf(step);
const isActive = currentIndex >= i;
return (
<motion.div
key={s}
initial={false}
animate={{
backgroundColor: isActive ? 'var(--active-color)' : 'var(--inactive-color)',
scale: currentIndex === i ? 1 : 0.9,
width: currentIndex === i ? 16 : 4,
opacity: isActive ? 1 : 0.3
}}
className="h-1 rounded-full transition-all duration-300"
style={{
// @ts-ignore
'--active-color': isActive ? '#171717' : '#e5e5e5',
'--inactive-color': '#e5e5e5'
} as React.CSSProperties}
/>
)
})}
</div>
)}
</div>
{/* Content */}
<div className="p-5">
<AnimatePresence mode="wait" custom={direction}>
{step === 'shipping' && (
<motion.div
key="shipping"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="space-y-3"
>
<div className="mb-1">
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">Shipping</h2>
<p className="text-[10px] text-neutral-500 mt-0.5">Where should we send your order?</p>
</div>
<div className="space-y-2.5">
<div className="group">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">Full Name</label>
<div className="relative group-focus-within:scale-[1.01] transition-transform duration-200">
<input
type="text"
placeholder="John Doe"
className="w-full pl-3 pr-3 py-2 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
<div className="absolute right-3 top-2.5 w-1.5 h-1.5 rounded-full bg-green-500 opacity-0 transition-opacity" style={{ opacity: formData.name.length > 2 ? 1 : 0 }} />
</div>
</div>
<div className="group">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">Address</label>
<input
type="text"
placeholder="123 Innovation Dr"
className="w-full px-3 py-2 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs"
value={formData.address}
onChange={(e) => setFormData({...formData, address: e.target.value})}
/>
</div>
<div className="grid grid-cols-2 gap-2.5">
<div className="group">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">City</label>
<input
type="text"
placeholder="San Francisco"
className="w-full px-3 py-2 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs"
value={formData.city}
onChange={(e) => setFormData({...formData, city: e.target.value})}
/>
</div>
<div className="group">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">ZIP Code</label>
<input
type="text"
placeholder="94103"
className="w-full px-3 py-2 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs"
value={formData.zip}
onChange={(e) => setFormData({...formData, zip: e.target.value})}
/>
</div>
</div>
</div>
<div className="pt-2 flex justify-end">
<button
onClick={handleNext}
className="group relative inline-flex h-9 items-center justify-center overflow-hidden rounded-lg bg-neutral-950 dark:bg-white px-5 font-medium text-white dark:text-neutral-950 shadow-lg transition-all hover:bg-neutral-800 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 text-xs"
>
<motion.div
className="absolute inset-0 bg-white/20"
initial={{ x: '-100%' }}
whileHover={{ x: '100%' }}
transition={{ duration: 0.5 }}
/>
<span className="flex items-center gap-1.5 relative z-10">
Continue <ChevronRight className="w-3 h-3" />
</span>
</button>
</div>
</motion.div>
)}
{step === 'payment' && (
<motion.div
key="payment"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="space-y-4"
>
<div className="mb-1">
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">Payment</h2>
<p className="text-[10px] text-neutral-500 mt-0.5">Secure credit card transaction.</p>
</div>
{/* Realistic 3D Card */}
<div className="w-full aspect-[1.586] perspective-1000 group cursor-pointer" onClick={() => setFocusedField(focusedField === 'cvv' ? 'cardNumber' : 'cvv')}>
<motion.div
className="w-full h-full relative preserve-3d transition-all duration-700"
animate={{ rotateY: focusedField === 'cvv' ? 180 : 0 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
{/* Front */}
<div className="absolute inset-0 backface-hidden rounded-xl p-4 text-white shadow-2xl flex flex-col justify-between overflow-hidden border border-white/10 bg-[#0a0a0a]">
{/* Texture & Gradients */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-30 mix-blend-overlay"></div>
<div className="absolute inset-0 bg-gradient-to-br from-neutral-900 via-[#1a1a1a] to-neutral-900"></div>
{/* Moving Beams */}
<div className="absolute inset-0 overflow-hidden">
<motion.div
animate={{
x: ['-100%', '200%'],
opacity: [0, 0.5, 0]
}}
transition={{
duration: 2.5,
repeat: Infinity,
repeatDelay: 1,
ease: "easeInOut"
}}
className="absolute top-0 bottom-0 w-32 bg-gradient-to-r from-transparent via-white/10 to-transparent skew-x-12 blur-xl"
/>
<motion.div
animate={{
x: ['-100%', '200%'],
opacity: [0, 0.3, 0]
}}
transition={{
duration: 2,
repeat: Infinity,
repeatDelay: 0.5,
ease: "easeInOut",
delay: 1
}}
className="absolute top-0 bottom-0 w-12 bg-gradient-to-r from-transparent via-blue-500/20 to-transparent skew-x-12 blur-lg"
/>
</div>
{/* Holographic Foil */}
<div className="absolute inset-0 bg-gradient-to-tr from-white/5 via-transparent to-white/5 opacity-50 mix-blend-overlay pointer-events-none"></div>
<div className="flex justify-between items-start z-10 relative">
{/* EMV Chip */}
<div className="w-9 h-6 rounded bg-gradient-to-br from-[#e0c388] via-[#d4af37] to-[#b8860b] flex items-center justify-center shadow-inner border border-[#d4af37]/50 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-white/40 to-transparent opacity-50"></div>
<div className="w-full h-[1px] bg-black/20 absolute top-1/3"></div>
<div className="w-full h-[1px] bg-black/20 absolute bottom-1/3"></div>
<div className="h-full w-[1px] bg-black/20 absolute left-1/3"></div>
<div className="h-full w-[1px] bg-black/20 absolute right-1/3"></div>
</div>
<div className="flex items-center gap-1">
<Wifi className="w-3.5 h-3.5 text-white/70 rotate-90" />
</div>
</div>
<div className="z-10 relative mt-1">
<div className="flex items-center gap-3 mb-2">
<p className="font-mono text-[15px] tracking-[0.14em] text-white/90 drop-shadow-md">
{formData.cardNumber || '4242 4242 4242 4242'}
</p>
</div>
<div className="flex items-end gap-8">
<div>
<p className="text-[6px] text-white/50 uppercase tracking-widest mb-0.5 font-semibold">Card Holder</p>
<p className="font-medium tracking-wide uppercase text-[9px] text-white/90">{formData.name || 'YOUR NAME'}</p>
</div>
<div>
<p className="text-[6px] text-white/50 uppercase tracking-widest mb-0.5 font-semibold">Expires</p>
<p className="font-medium tracking-wide text-[9px] text-white/90">{formData.expiry || 'MM/YY'}</p>
</div>
</div>
</div>
<div className="absolute bottom-4 right-4 z-10">
<span className="font-black italic tracking-wider text-white text-lg drop-shadow-md opacity-90">VISA</span>
</div>
</div>
{/* Back */}
<div
className="absolute inset-0 backface-hidden bg-[#0a0a0a] rounded-xl text-white shadow-xl overflow-hidden border border-white/10"
style={{ transform: "rotateY(180deg)" }}
>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-30 mix-blend-overlay"></div>
<div className="w-full h-8 bg-black mt-4 relative"></div>
<div className="p-4 relative z-10">
<div className="flex flex-col items-end">
<p className="text-[7px] text-white/60 uppercase tracking-wider mb-1 mr-1">CVV</p>
<div className="w-full h-7 bg-white text-black font-mono flex items-center justify-end px-2 rounded shadow-inner text-xs font-bold">
{formData.cvv || '123'}
</div>
</div>
<div className="mt-3 flex items-center gap-2 opacity-60">
<div className="w-6 h-4 bg-white/10 rounded flex items-center justify-center border border-white/10">
<div className="w-4 h-2 border border-white/30 rounded-sm"></div>
</div>
<span className="text-[7px] max-w-[150px] leading-tight">Secure Transaction. Authorized Signature Not Required.</span>
</div>
</div>
</div>
</motion.div>
</div>
<div className="space-y-2.5 p-1">
<div className="group relative">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">Card Number</label>
<div className="relative group-focus-within:scale-[1.01] transition-transform duration-200">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-0 group-focus-within:opacity-20 transition-opacity duration-500"></div>
<div className="relative bg-white dark:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden flex items-center">
<div className="pl-3 text-neutral-400">
<CreditCard className="w-3.5 h-3.5" />
</div>
<input
type="text"
placeholder="0000 0000 0000 0000"
maxLength={19}
className="w-full pl-2 pr-3 py-2 bg-transparent focus:outline-none font-mono text-xs"
value={formData.cardNumber}
onChange={(e) => setFormData({...formData, cardNumber: e.target.value})}
onFocus={() => setFocusedField('cardNumber')}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2.5">
<div className="group relative">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">Expiry Date</label>
<input
type="text"
placeholder="MM/YY"
maxLength={5}
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs font-mono"
value={formData.expiry}
onChange={(e) => setFormData({...formData, expiry: e.target.value})}
onFocus={() => setFocusedField('expiry')}
/>
</div>
<div className="group relative">
<label className="text-[9px] font-bold text-neutral-500 uppercase tracking-wider mb-1 block">CVV</label>
<input
type="text"
placeholder="123"
maxLength={3}
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white transition-all shadow-sm focus:shadow-md text-xs font-mono"
value={formData.cvv}
onChange={(e) => setFormData({...formData, cvv: e.target.value})}
onFocus={() => setFocusedField('cvv')}
/>
</div>
</div>
</div>
<div className="flex justify-between pt-2">
<button
onClick={handleBack}
className="text-xs text-neutral-500 hover:text-neutral-900 dark:hover:text-white font-medium flex items-center gap-1 transition-colors"
>
<ChevronLeft className="w-3 h-3" /> Back
</button>
<button
onClick={handleNext}
className="group relative inline-flex h-9 items-center justify-center overflow-hidden rounded-lg bg-neutral-950 dark:bg-white px-5 font-medium text-white dark:text-neutral-950 shadow-lg transition-all hover:bg-neutral-800 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 text-xs"
>
<span className="flex items-center gap-1.5 relative z-10">
Review <ChevronRight className="w-3 h-3" />
</span>
</button>
</div>
</motion.div>
)}
{step === 'review' && (
<motion.div
key="review"
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="space-y-4"
>
<div className="mb-1">
<h2 className="text-lg font-bold text-neutral-900 dark:text-white">Review</h2>
<p className="text-[10px] text-neutral-500 mt-0.5">Please verify your information.</p>
</div>
<div className="space-y-3">
<div className="bg-neutral-50 dark:bg-neutral-800/30 rounded-xl p-3 border border-neutral-100 dark:border-white/5 space-y-3">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-blue-500/10 flex items-center justify-center shrink-0">
<MapPin className="w-4 h-4 text-blue-500" />
</div>
<div>
<p className="text-[10px] text-neutral-500 font-bold uppercase tracking-wider mb-0.5">Shipping To</p>
<p className="text-xs font-medium text-neutral-900 dark:text-white">{formData.name || 'John Doe'}</p>
<p className="text-xs text-neutral-500">{formData.address || '123 Innovation Dr'}, {formData.city || 'San Francisco'} {formData.zip || '94103'}</p>
</div>
</div>
<div className="h-px bg-neutral-200 dark:bg-white/5 w-full"></div>
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-purple-500/10 flex items-center justify-center shrink-0">
<CreditCard className="w-4 h-4 text-purple-500" />
</div>
<div>
<p className="text-[10px] text-neutral-500 font-bold uppercase tracking-wider mb-0.5">Payment Method</p>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-neutral-900 dark:text-white">Visa ending in {formData.cardNumber.slice(-4) || '4242'}</span>
<div className="px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-700 text-[9px] font-mono">
{formData.expiry || '12/25'}
</div>
</div>
</div>
</div>
</div>
<div className="bg-neutral-50 dark:bg-neutral-800/30 rounded-xl p-3 border border-neutral-100 dark:border-white/5">
<div className="flex justify-between items-center mb-1.5">
<span className="text-xs text-neutral-500">Subtotal</span>
<span className="text-xs font-medium text-neutral-900 dark:text-white">$129.00</span>
</div>
<div className="flex justify-between items-center mb-1.5">
<span className="text-xs text-neutral-500">Shipping</span>
<span className="text-xs font-medium text-green-600">Free</span>
</div>
<div className="flex justify-between items-center mb-1.5">
<span className="text-xs text-neutral-500">Tax</span>
<span className="text-xs font-medium text-neutral-900 dark:text-white">$12.90</span>
</div>
<div className="h-px bg-neutral-200 dark:bg-white/5 w-full my-2"></div>
<div className="flex justify-between items-center">
<span className="text-sm font-bold text-neutral-900 dark:text-white">Total</span>
<span className="text-sm font-bold text-neutral-900 dark:text-white">$141.90</span>
</div>
</div>
</div>
<div className="flex justify-between pt-2">
<button
onClick={handleBack}
className="text-xs text-neutral-500 hover:text-neutral-900 dark:hover:text-white font-medium flex items-center gap-1 transition-colors"
>
<ChevronLeft className="w-3 h-3" /> Back
</button>
<button
onClick={handleSubmit}
disabled={loading}
className="group relative inline-flex h-9 items-center justify-center overflow-hidden rounded-lg bg-neutral-950 dark:bg-white px-6 font-medium text-white dark:text-neutral-950 shadow-lg transition-all hover:bg-neutral-800 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 text-xs disabled:opacity-70 disabled:cursor-not-allowed min-w-[120px]"
>
{loading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<span className="flex items-center gap-1.5 relative z-10">
Pay $141.90 <ShieldCheck className="w-3 h-3" />
</span>
)}
{!loading && (
<motion.div
className="absolute inset-0 bg-white/20"
initial={{ x: '-100%' }}
whileHover={{ x: '100%' }}
transition={{ duration: 0.5 }}
/>
)}
</button>
</div>
</motion.div>
)}
{step === 'success' && (
<motion.div
key="success"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", duration: 0.8 }}
className="flex flex-col items-center justify-center py-8 text-center"
>
<div className="w-20 h-20 rounded-full bg-green-500/10 flex items-center justify-center mb-4 relative">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.2 }}
>
<div className="w-16 h-16 rounded-full bg-green-500 flex items-center justify-center shadow-lg shadow-green-500/30">
<Check className="w-8 h-8 text-white stroke-[3px]" />
</div>
</motion.div>
<div className="absolute inset-0 border-2 border-green-500/20 rounded-full animate-ping"></div>
</div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-white mb-2">Order Confirmed!</h2>
<p className="text-xs text-neutral-500 max-w-[200px] leading-relaxed mb-6">
Thank you for your purchase. A confirmation email has been sent to your inbox.
</p>
<div className="flex gap-3">
<button
onClick={() => {
setStep('shipping')
setFormData({
name: '',
address: '',
city: '',
zip: '',
cardNumber: '',
expiry: '',
cvv: ''
})
}}
className="text-xs text-neutral-500 hover:text-neutral-900 dark:hover:text-white font-medium"
>
Start New Order
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| step | 'shipping' | 'payment' | 'review' | 'success' | 'shipping' | Current step in the flow. |
| onNext | () => void | undefined | Callback to advance to the next step. |
| onSuccess | () => void | undefined | Callback fired when checkout completes. |
| 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.

