Components
Verification code input with auto-advance, backspace navigation, full paste support, and animated states. Each digit box auto-focuses the next on entry. Backspace clears and moves back. Paste fills all fields instantly. Animated focus ring with subtle glow, pop animation when a digit lands, blinking cursor in empty focused box. Error state triggers shake animation. Success state pulses each digit sequentially. Optional separator dash between groups. Masked mode for PIN entry. Three sizes. Fires onComplete when all digits are filled. Built on Framer Motion.
Loading preview...
import { Component as OTPInput } from "@/components/ui/otp-input";
import { useState, useCallback } from "react";
import { Shield, Loader2, Check, X } from "lucide-react";
const CORRECT_CODE = "123456";
function VerificationDemo() {
const [status, setStatus] = useState<"idle" | "verifying" | "success" | "error">("idle");
const handleComplete = useCallback((otp: string) => {
setStatus("verifying");
// Simulate API call
setTimeout(() => {
if (otp === CORRECT_CODE) {
setStatus("success");
} else {
setStatus("error");
setTimeout(() => setStatus("idle"), 1500);
}
}, 800);
}, []);
return (
<div className="flex flex-col items-center gap-6 w-full">
<div className="flex items-center justify-center size-12 rounded-full bg-white/5 border border-white/10">
<Shield className="size-5 text-white/60" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-white mb-1">
Verification code
</h3>
<p className="text-sm text-neutral-400">
Enter the 6-digit code sent to your email
</p>
</div>
<OTPInput
length={6}
separatorAfter={2}
onComplete={handleComplete}
error={status === "error"}
success={status === "success"}
disabled={status === "verifying" || status === "success"}
size="md"
/>
<div className="h-6 flex items-center">
{status === "verifying" && (
<div className="flex items-center gap-2 text-sm text-neutral-400">
<Loader2 className="size-4 animate-spin" />
Verifying...
</div>
)}
{status === "success" && (
<div className="flex items-center gap-2 text-sm text-emerald-400">
<Check className="size-4" />
Verified successfully
</div>
)}
{status === "error" && (
<div className="flex items-center gap-2 text-sm text-red-400">
<X className="size-4" />
Invalid code. Try 123456.
</div>
)}
{status === "idle" && (
<p className="text-xs text-neutral-600">
Try <span className="font-mono text-neutral-400">123456</span> for success
</p>
)}
</div>
</div>
);
}
export default function Demo() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-[#030712] px-6 py-16">
<div className="w-full max-w-lg mx-auto flex flex-col items-center gap-16">
{/* Main demo — full verification flow */}
<div className="w-full rounded-2xl border border-white/10 bg-white/[0.01] p-8 flex flex-col items-center">
<VerificationDemo />
</div>
{/* Sizes */}
<div className="w-full flex flex-col items-center gap-6">
<p className="text-xs font-semibold text-white/30 uppercase tracking-wider">
Sizes
</p>
<div className="flex flex-col items-center gap-5">
<OTPInput length={4} size="sm" />
<OTPInput length={4} size="md" />
<OTPInput length={4} size="lg" />
</div>
</div>
{/* Masked */}
<div className="w-full flex flex-col items-center gap-4">
<p className="text-xs font-semibold text-white/30 uppercase tracking-wider">
Masked (PIN entry)
</p>
<OTPInput length={4} masked size="lg" />
</div>
</div>
</div>
);
}