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-react

How 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";
2
3import { useState, useRef, useEffect } from "react";
4import { ShieldCheck, KeyRound } from "lucide-react";
5
6export 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)[]>([]);
12
13 useEffect(() => {
14 if (countdown <= 0) return;
15 const t = setTimeout(() => setCountdown(countdown - 1), 1000);
16 return () => clearTimeout(t);
17 }, [countdown]);
18
19 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 };
26
27 const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
28 if (e.key === "Backspace" && !code[index] && index > 0) {
29 inputRefs.current[index - 1]?.focus();
30 }
31 };
32
33 const minutes = Math.floor(countdown / 60);
34 const seconds = countdown % 60;
35
36 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 }
50
51 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>
58
59 <div className="mb-4 flex justify-center gap-2">
60 {code.map((digit, i) => (
61 <input
62 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>
74
75 <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>
82
83 <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}

Related Auth & Onboarding Components

Command Palette

Search for a command to run...