PIN Input
ComponentsA segmented verification code and one-time password (OTP) input with auto-advance, backspace navigation, and clipboard paste support.
One-Time Password (OTP) Input
Designed for two-factor authentication (2FA) and account verification. Handles digit validation, automatic focus stepping, backwards backspace navigation, and full-clipboard paste detection.
-
Live Functional Playground Interactive Hooks • useState
6-Digit Verification Code
Type digits to auto-advance, backspace to reverse
-
Status: 3 of 6 digits
Motion Physics & Animation Showcase Framer Motion ✨
Motion Physics
✨
-
Hover to trigger spring elevation lift
Format:
Required Animation Library: framer-motion & lucide-react
Install this package in your project before importing the animated component.
npm i framer-motion lucide-react <div class="flex items-center gap-2 sm:gap-3">
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" value="4" />
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" value="8" />
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" value="2" />
<span class="text-muted-foreground font-semibold px-1">-</span>
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" placeholder="•" />
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" placeholder="•" />
<input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" class="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all" placeholder="•" />
</div> import React from "react";
export function PinInput({ length = 6 }: { length?: number }) {
return (
<div className="flex items-center gap-2">
{Array.from({ length }).map((_, index) => (
<input
key={index}
type="text"
maxLength={1}
inputMode="numeric"
pattern="[0-9]*"
className="w-11 h-13 text-center text-lg font-bold rounded-xl border border-input bg-background text-foreground shadow-2xs focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
/>
))}
</div>
);
} "use client";
import React, { useState, useRef } from "react";
export interface PinInputProps {
length?: number;
onComplete?: (code: string) => void;
autoFocus?: boolean;
}
export default function PinInput({
length = 6,
onComplete,
autoFocus = true,
}: PinInputProps) {
const [digits, setDigits] = useState<string[]>(Array(length).fill(""));
const [isVerifying, setIsVerifying] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handleChange = (index: number, value: string) => {
// Only accept numeric inputs
const lastChar = value.slice(-1);
if (value && !/^\d$/.test(lastChar)) return;
const newDigits = [...digits];
newDigits[index] = lastChar;
setDigits(newDigits);
// Auto-advance focus to next cell
if (lastChar && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
// Check completion
const fullCode = newDigits.join("");
if (fullCode.length === length && !newDigits.includes("")) {
setIsVerifying(true);
setTimeout(() => {
setIsVerifying(false);
setIsSuccess(true);
onComplete?.(fullCode);
}, 800);
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace") {
if (!digits[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
const newDigits = [...digits];
newDigits[index - 1] = "";
setDigits(newDigits);
} else {
const newDigits = [...digits];
newDigits[index] = "";
setDigits(newDigits);
}
} else if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1]?.focus();
} else if (e.key === "ArrowRight" && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
if (!pasted) return;
const newDigits = [...digits];
pasted.split("").forEach((char, i) => {
newDigits[i] = char;
});
setDigits(newDigits);
const targetFocus = Math.min(pasted.length, length - 1);
inputRefs.current[targetFocus]?.focus();
if (pasted.length === length) {
setIsVerifying(true);
setTimeout(() => {
setIsVerifying(false);
setIsSuccess(true);
onComplete?.(pasted);
}, 800);
}
};
const handleReset = () => {
setDigits(Array(length).fill(""));
setIsSuccess(false);
inputRefs.current[0]?.focus();
};
return (
<div className="w-full max-w-sm space-y-4 p-5 rounded-2xl border border-border bg-card shadow-sm text-center">
<div className="space-y-1">
<h4 className="text-sm font-bold text-foreground">Verification Code</h4>
<p className="text-xs text-muted-foreground">
Enter the 6-digit confirmation code sent to your device.
</p>
</div>
<div className="flex items-center justify-center gap-2 sm:gap-2.5">
{digits.map((digit, index) => (
<React.Fragment key={index}>
<input
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
autoFocus={autoFocus && index === 0}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
className={`w-10 h-13 sm:w-11 sm:h-14 text-center text-lg font-bold rounded-xl border bg-background text-foreground transition-all focus:outline-none focus:ring-2 ${
isSuccess
? "border-green-500 ring-green-500/20 text-green-500"
: digit
? "border-primary ring-primary/20"
: "border-input hover:border-border"
}`}
/>
{index === 2 && <span className="text-muted-foreground font-semibold px-0.5">-</span>}
</React.Fragment>
))}
</div>
{isVerifying && (
<p className="text-xs text-primary font-medium animate-pulse">Verifying code...</p>
)}
{isSuccess && (
<div className="flex items-center justify-center gap-2 text-xs text-green-600 dark:text-green-400 font-semibold">
<span>✓ Verification Successful</span>
<button
type="button"
onClick={handleReset}
className="text-[11px] underline text-muted-foreground hover:text-foreground ml-2"
>
Reset
</button>
</div>
)}
</div>
);
} "use client";
import React, { useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { CheckCircle2, ShieldCheck } from "lucide-react";
export default function AnimatedPinInput() {
const [digits, setDigits] = useState<string[]>(["", "", "", ""]);
const [completed, setCompleted] = useState(false);
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handleChange = (index: number, val: string) => {
const char = val.slice(-1);
if (val && !/^\d$/.test(char)) return;
const next = [...digits];
next[index] = char;
setDigits(next);
if (char && index < 3) {
inputRefs.current[index + 1]?.focus();
}
if (next.every((d) => d !== "")) {
setTimeout(() => setCompleted(true), 400);
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace" && !digits[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
return (
<div className="w-full max-w-sm p-6 rounded-2xl border border-border bg-card/70 backdrop-blur-md shadow-xl text-center space-y-5">
<div className="flex justify-center">
<motion.div
animate={{ scale: [1, 1.08, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="p-3 rounded-full bg-primary/10 text-primary border border-primary/20"
>
<ShieldCheck className="h-6 w-6" />
</motion.div>
</div>
<div className="space-y-1">
<h4 className="text-sm font-bold text-foreground">Secure Passcode</h4>
<p className="text-xs text-muted-foreground">Enter your 4-digit security PIN</p>
</div>
<div className="flex justify-center gap-3">
{digits.map((digit, i) => (
<motion.div
key={i}
animate={{ scale: digit ? [1, 1.15, 1] : 1 }}
transition={{ type: "spring", stiffness: 500, damping: 25 }}
>
<input
ref={(el) => { inputRefs.current[i] = el; }}
type="text"
maxLength={1}
inputMode="numeric"
value={digit}
onChange={(e) => handleChange(i, e.target.value)}
onKeyDown={(e) => handleKeyDown(i, e)}
className="w-12 h-14 text-center text-xl font-bold rounded-xl border border-border bg-background focus:border-primary focus:ring-2 focus:ring-primary/25 outline-none transition-all shadow-xs"
/>
</motion.div>
))}
</div>
<AnimatePresence>
{completed && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
className="flex items-center justify-center gap-2 text-xs font-semibold text-green-500 bg-green-500/10 py-2 rounded-xl border border-green-500/20"
>
<CheckCircle2 className="h-4 w-4" />
<span>Passcode Verified!</span>
</motion.div>
)}
</AnimatePresence>
</div>
);
} Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Total count of digit cells to render |
autoFocus | boolean | true | Automatically focuses the first cell upon mount |
onComplete | (code: string) => void | undefined | Callback invoked with the full string once all cells are filled |
Usability & Accessibility Features
- Keyboard Traversal: Supports Left Arrow and Right Arrow to easily move between cells without wiping values.
- Backspace Ergonomics: Pressing Backspace on an empty cell automatically shifts focus to the previous cell and deletes its content.
- Mobile Optimized: Uses
inputMode="numeric"to reliably summon the numeric keypad on iOS and Android devices. - Paste Detection: Automatically parses pasted verification SMS messages or clipboard digits across all cells in order.