Back to Component Explorer

OTP / Magic Link Sign In

signin

Two-step: email then 6-digit OTP input boxes to verify.

#signin#otp#magic#passwordless
Live Interactive Preview

Magic Link

We'll send a 6-digit code to your email

Component Source Code

Select language in the dropdown
OTPSignIn.tsx
1'use client';
2import React, { useState, useRef } from 'react';
3import { ArrowRight } from 'lucide-react';
4
5export default function OTPSignIn() {
6 const [email, setEmail] = useState('');
7 const [step, setStep] = useState<'email' | 'otp'>('email');
8 const [otp, setOtp] = useState(['', '', '', '', '', '']);
9 const refs = [useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null)];
10
11 const handleOtp = (i: number, val: string) => {
12 if (!/^\d?$/.test(val)) return;
13 const next = [...otp];
14 next[i] = val;
15 setOtp(next);
16 if (val && i < 5) refs[i + 1].current?.focus();
17 };
18
19 const handleKey = (i: number, e: React.KeyboardEvent) => {
20 if (e.key === 'Backspace' && !otp[i] && i > 0) refs[i - 1].current?.focus();
21 };
22
23 return (
24 <div className="w-full max-w-sm rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-8 shadow-lg">
25 {step === 'email' ? (
26 <>
27 <div className="mb-6 text-center">
28 <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl border-2 border-blue-600 text-blue-600">
29 <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
30 </div>
31 <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Magic Link</h2>
32 <p className="mt-1 text-xs text-zinc-500">We&apos;ll send a 6-digit code to your email</p>
33 </div>
34 <form onSubmit={(e) => { e.preventDefault(); setStep('otp'); }} className="space-y-4">
35 <div>
36 <label className="block text-xs font-semibold text-zinc-700 dark:text-zinc-300 mb-1.5">Email address</label>
37 <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required placeholder="you@example.com" className="w-full rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800 px-4 py-2.5 text-sm text-zinc-900 dark:text-zinc-100 placeholder:text-zinc-400 outline-none focus:border-blue-500 transition" />
38 </div>
39 <button type="submit" className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 hover:bg-blue-700 py-2.5 text-sm font-semibold text-white transition">
40 Send Code <ArrowRight className="h-4 w-4" />
41 </button>
42 </form>
43 </>
44 ) : (
45 <>
46 <div className="mb-6 text-center">
47 <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Enter code</h2>
48 <p className="mt-1 text-xs text-zinc-500">Sent to <span className="font-semibold text-zinc-700 dark:text-zinc-300">{email}</span></p>
49 </div>
50 <div className="flex justify-center gap-2 mb-6">
51 {otp.map((d, i) => (
52 <input
53 key={i}
54 ref={refs[i]}
55 type="text"
56 inputMode="numeric"
57 maxLength={1}
58 value={d}
59 onChange={(e) => handleOtp(i, e.target.value)}
60 onKeyDown={(e) => handleKey(i, e)}
61 className="h-12 w-10 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800 text-center text-lg font-bold text-zinc-900 dark:text-zinc-100 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition"
62 />
63 ))}
64 </div>
65 <button
66 onClick={() => { if (otp.join('').length === 6) alert('OTP Verified!'); }}
67 className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 hover:bg-blue-700 py-2.5 text-sm font-semibold text-white transition"
68 >
69 Verify Code <ArrowRight className="h-4 w-4" />
70 </button>
71 <button onClick={() => setStep('email')} className="mt-3 w-full text-center text-xs text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 transition">← Change email</button>
72 </>
73 )}
74 </div>
75 );
76}

Installation & Integration Guide

Step 1: Tailwind CSS v4 Setup
Tailwind Docs

If you do not have Tailwind installed yet, run this installation command:

Terminal
npm install tailwindcss @tailwindcss/postcss postcss

Then add the Tailwind directive to your main stylesheet (globals.css):

@import "tailwindcss";

Step 2: Install Icon Dependencies

npm install lucide-react react-icons

Step 3: Add Component File

Create a new file in your project at src/components/signin/OTPSignIn.tsx and paste the copied code from the source viewer above.

Step 4: Usage Example

ExamplePage.tsx
1import OTP/MagicLinkSignIn from '@/components/signin/OTPSignIn';
2
3export default function ExamplePage() {
4 return (
5 <main className="p-8 flex items-center justify-center min-h-screen">
6 <OTP/MagicLinkSignIn />
7 </main>
8 );
9}