- Gallery
- Auth & Onboarding
- TwoFactorInput
New
TwoFactorInput
2FA verification with 6-digit code input boxes, countdown timer, and backup code option.
Auth & Onboarding2faverificationsecurityotpauth
Dependencies
shadcn/ui components needed:
npx shadcn@latest add lucide-reactHow to use this component
Copy the code below into your project. Make sure you have the required shadcn/ui dependencies installed. Then import and use the component in your pages or layouts.
Code
1"use client";23import { useState, useRef, useEffect } from "react";4import { ShieldCheck, KeyRound } from "lucide-react";56export default function TwoFactorInput() {7 const [code, setCode] = useState(Array(6).fill(""));8 const [countdown, setCountdown] = useState(120);9 const [showBackup, setShowBackup] = useState(false);10 const [backupCode, setBackupCode] = useState("");11 const inputRefs = useRef<(HTMLInputElement | null)[]>([]);1213 useEffect(() => {14 if (countdown <= 0) return;15 const t = setTimeout(() => setCountdown(countdown - 1), 1000);16 return () => clearTimeout(t);17 }, [countdown]);1819 const handleChange = (index: number, value: string) => {20 if (!/^\d*$/.test(value)) return;21 const next = [...code];22 next[index] = value.slice(-1);23 setCode(next);24 if (value && index < 5) inputRefs.current[index + 1]?.focus();25 };2627 const handleKeyDown = (index: number, e: React.KeyboardEvent) => {28 if (e.key === "Backspace" && !code[index] && index > 0) {29 inputRefs.current[index - 1]?.focus();30 }31 };3233 const minutes = Math.floor(countdown / 60);34 const seconds = countdown % 60;3536 if (showBackup) {37 return (38 <div className="mx-auto w-full max-w-sm rounded-2xl border border-zinc-200 bg-white p-8 text-center shadow-xl dark:border-zinc-800 dark:bg-zinc-950">39 <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-amber-50 dark:bg-amber-900/20">40 <KeyRound className="h-7 w-7 text-amber-600 dark:text-amber-400" />41 </div>42 <h2 className="mb-1 text-lg font-bold text-zinc-900 dark:text-zinc-50">Backup code</h2>43 <p className="mb-6 text-sm text-zinc-500 dark:text-zinc-400">Enter one of your backup recovery codes</p>44 <input type="text" placeholder="xxxx-xxxx-xxxx" value={backupCode} onChange={(e) => setBackupCode(e.target.value)} className="mb-4 w-full rounded-lg border border-zinc-300 bg-transparent px-3.5 py-2.5 text-center font-mono text-sm tracking-widest outline-none transition-colors placeholder:text-zinc-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-zinc-700 dark:text-zinc-100" />45 <button className="mb-3 w-full rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-violet-500 active:scale-[0.98]">Verify backup code</button>46 <button onClick={() => setShowBackup(false)} className="text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200">Back to 2FA code</button>47 </div>48 );49 }5051 return (52 <div className="mx-auto w-full max-w-sm rounded-2xl border border-zinc-200 bg-white p-8 text-center shadow-xl dark:border-zinc-800 dark:bg-zinc-950">53 <div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-violet-50 dark:bg-violet-900/20">54 <ShieldCheck className="h-7 w-7 text-violet-600 dark:text-violet-400" />55 </div>56 <h2 className="mb-1 text-lg font-bold text-zinc-900 dark:text-zinc-50">Two-factor authentication</h2>57 <p className="mb-6 text-sm text-zinc-500 dark:text-zinc-400">Enter the 6-digit code from your authenticator app</p>5859 <div className="mb-4 flex justify-center gap-2">60 {code.map((digit, i) => (61 <input62 key={i}63 ref={(el) => { inputRefs.current[i] = el; }}64 type="text"65 inputMode="numeric"66 maxLength={1}67 value={digit}68 onChange={(e) => handleChange(i, e.target.value)}69 onKeyDown={(e) => handleKeyDown(i, e)}70 className="h-12 w-10 rounded-lg border border-zinc-300 bg-transparent text-center font-mono text-lg font-semibold outline-none transition-all focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 dark:border-zinc-700 dark:text-zinc-100"71 />72 ))}73 </div>7475 <div className="mb-6 text-sm text-zinc-500 dark:text-zinc-400">76 {countdown > 0 ? (77 <span>Code expires in <span className="font-mono font-semibold text-violet-600 dark:text-violet-400">{minutes}:{seconds.toString().padStart(2, "0")}</span></span>78 ) : (79 <button className="font-semibold text-violet-600 hover:text-violet-500 dark:text-violet-400">Request new code</button>80 )}81 </div>8283 <button className="mb-4 w-full rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-violet-500 active:scale-[0.98]">Verify</button>84 <button onClick={() => setShowBackup(true)} className="text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200">Use a backup code</button>85 </div>86 );87}