OTP Input
Original · free6-box one-time-code input with paste
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/inp-otp.json"use client";
import {
useCallback,
useEffect,
useId,
useRef,
useState,
type ChangeEvent,
type ClipboardEvent,
type FocusEvent,
type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
/* ------------------------------------------------------------------ */
/* helpers */
/* ------------------------------------------------------------------ */
const EMPTY = " ";
type Tone = "default" | "error" | "success";
function toCells(value: string, length: number): string[] {
return value.padEnd(length, EMPTY).slice(0, length).split("");
}
function digitsOf(value: string): string {
return value.replace(/[^0-9]/g, "");
}
function boxClasses(tone: Tone, filled: boolean): string {
const base =
"peer h-14 w-11 rounded-xl border text-center text-2xl font-semibold tabular-nums caret-transparent outline-none transition-[color,background-color,border-color,box-shadow,transform] duration-150 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-60 dark:focus-visible:ring-offset-slate-900 sm:h-16 sm:w-12";
if (tone === "error") {
return `${base} border-rose-400 bg-rose-50/70 text-rose-700 focus-visible:border-rose-500 focus-visible:ring-rose-500 dark:border-rose-500/70 dark:bg-rose-500/10 dark:text-rose-200`;
}
if (tone === "success") {
return `${base} border-emerald-400 bg-emerald-50/70 text-emerald-700 focus-visible:border-emerald-500 focus-visible:ring-emerald-500 dark:border-emerald-500/70 dark:bg-emerald-500/10 dark:text-emerald-200`;
}
if (filled) {
return `${base} border-indigo-400 bg-indigo-50/70 text-slate-900 focus-visible:border-indigo-500 focus-visible:ring-indigo-500 dark:border-indigo-500/70 dark:bg-indigo-500/10 dark:text-slate-50`;
}
return `${base} border-slate-300 bg-white text-slate-900 hover:border-slate-400 focus-visible:border-indigo-500 focus-visible:ring-indigo-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:border-slate-600`;
}
/* ------------------------------------------------------------------ */
/* inline icons */
/* ------------------------------------------------------------------ */
function EnvelopeIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
<rect x="3" y="5" width="18" height="14" rx="2.5" />
<path d="m3.5 7 7.4 5.3a2 2 0 0 0 2.2 0L20.5 7" />
</svg>
);
}
function ShieldIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
<path d="M12 3 5 6v5.5c0 4.3 2.9 7.5 7 9 4.1-1.5 7-4.7 7-9V6l-7-3Z" />
<path d="m9 12 2 2 4-4" />
</svg>
);
}
function EyeIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
function EyeOffIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
<path d="M3 3l18 18" />
<path d="M10.6 6.2A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a15.6 15.6 0 0 1-3 3.6" />
<path d="M6.4 7.9A15.4 15.4 0 0 0 2.5 12S6 18.5 12 18.5a9.3 9.3 0 0 0 3.7-.8" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
</svg>
);
}
function CheckDrawIcon({ reduce }: { reduce: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
strokeDasharray={24}
strokeDashoffset={reduce ? 0 : 24}
className={reduce ? undefined : "otp6-check"}
/>
</svg>
);
}
function SpinnerIcon({ reduce }: { reduce: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" className={`h-4 w-4 ${reduce ? "" : "animate-spin"}`} aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth={3} className="opacity-25" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth={3} strokeLinecap="round" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* OTP field */
/* ------------------------------------------------------------------ */
type OtpFieldProps = {
length: number;
value: string;
onChange: (next: string) => void;
onComplete?: (code: string) => void;
mask?: boolean;
reveal?: boolean;
disabled?: boolean;
tone?: Tone;
shake?: boolean;
onShakeEnd?: () => void;
ariaLabel: string;
describedById?: string;
autoFocus?: boolean;
};
function OtpField({
length,
value,
onChange,
onComplete,
mask = false,
reveal = false,
disabled = false,
tone = "default",
shake = false,
onShakeEnd,
ariaLabel,
describedById,
autoFocus = false,
}: OtpFieldProps) {
const inputsRef = useRef<Array<HTMLInputElement | null>>([]);
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
const cells = toCells(value, length);
const focusBox = useCallback((index: number) => {
const el = inputsRef.current[index];
if (el) {
el.focus();
el.select();
}
}, []);
const commit = useCallback(
(nextCells: string[]) => {
const next = nextCells.join("");
onChange(next);
if (digitsOf(next).length === length) {
onComplete?.(digitsOf(next));
}
},
[length, onChange, onComplete]
);
const handleChange = (index: number, event: ChangeEvent<HTMLInputElement>) => {
const typed = digitsOf(event.target.value);
if (!typed) return;
const nextCells = toCells(value, length);
let cursor = index;
for (const digit of typed) {
if (cursor >= length) break;
nextCells[cursor] = digit;
cursor += 1;
}
commit(nextCells);
focusBox(Math.min(cursor, length - 1));
};
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
const nextCells = toCells(value, length);
switch (event.key) {
case "Backspace": {
event.preventDefault();
if (nextCells[index] !== EMPTY) {
nextCells[index] = EMPTY;
commit(nextCells);
} else if (index > 0) {
nextCells[index - 1] = EMPTY;
commit(nextCells);
focusBox(index - 1);
}
break;
}
case "Delete": {
event.preventDefault();
nextCells[index] = EMPTY;
commit(nextCells);
break;
}
case "ArrowLeft": {
event.preventDefault();
if (index > 0) focusBox(index - 1);
break;
}
case "ArrowRight": {
event.preventDefault();
if (index < length - 1) focusBox(index + 1);
break;
}
case "Home": {
event.preventDefault();
focusBox(0);
break;
}
case "End": {
event.preventDefault();
focusBox(length - 1);
break;
}
default:
break;
}
};
const handlePaste = (event: ClipboardEvent<HTMLInputElement>) => {
event.preventDefault();
const pasted = digitsOf(event.clipboardData.getData("text")).slice(0, length);
if (!pasted) return;
const nextCells = Array.from({ length }, (_, k) => pasted[k] ?? EMPTY);
commit(nextCells);
focusBox(Math.min(pasted.length, length - 1));
};
const handleGroupBlur = (event: FocusEvent<HTMLDivElement>) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
setFocusedIndex(null);
}
};
return (
<div
role="group"
aria-label={ariaLabel}
aria-describedby={describedById}
onBlur={handleGroupBlur}
className={`flex items-center gap-2 sm:gap-3 ${shake ? "otp6-shake" : ""}`}
onAnimationEnd={onShakeEnd}
>
{cells.map((char, index) => {
const isEmpty = char === EMPTY;
const showCaret = focusedIndex === index && isEmpty && !disabled;
const display = isEmpty ? "" : char;
const type = mask && !reveal ? "password" : "text";
return (
<div key={index} className="relative">
<input
ref={(el) => {
inputsRef.current[index] = el;
}}
type={type}
inputMode="numeric"
autoComplete={index === 0 ? "one-time-code" : "off"}
pattern="[0-9]*"
maxLength={1}
spellCheck={false}
autoCapitalize="off"
disabled={disabled}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={autoFocus && index === 0}
value={display}
aria-label={`${ariaLabel}, digit ${index + 1} of ${length}`}
aria-invalid={tone === "error"}
className={boxClasses(tone, !isEmpty)}
onChange={(event) => handleChange(index, event)}
onKeyDown={(event) => handleKeyDown(index, event)}
onPaste={handlePaste}
onFocus={(event) => {
setFocusedIndex(index);
event.currentTarget.select();
}}
/>
{showCaret ? (
<span
aria-hidden="true"
className="otp6-caret-el pointer-events-none absolute left-1/2 top-1/2 h-6 w-[2px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-500 dark:bg-indigo-400"
/>
) : null}
</div>
);
})}
</div>
);
}
/* ------------------------------------------------------------------ */
/* showcase */
/* ------------------------------------------------------------------ */
const DEMO_CODE = "482913";
const DEMO_PIN = "7391";
type Status = "idle" | "verifying" | "success" | "error";
function formatCountdown(total: number): string {
const minutes = Math.floor(total / 60);
const seconds = String(total % 60).padStart(2, "0");
return `${minutes}:${seconds}`;
}
export default function OneTimeCodeInput() {
const prefersReduced = useReducedMotion();
const reduce = Boolean(prefersReduced);
const hintId = useId();
const pinHintId = useId();
/* --- email verification card --- */
const [code, setCode] = useState("");
const [status, setStatus] = useState<Status>("idle");
const [shake, setShake] = useState(false);
const [seconds, setSeconds] = useState(29);
const timerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) window.clearTimeout(timerRef.current);
};
}, []);
useEffect(() => {
if (seconds <= 0) return;
const id = window.setTimeout(() => setSeconds((s) => s - 1), 1000);
return () => window.clearTimeout(id);
}, [seconds]);
const runVerify = useCallback(
(candidate: string) => {
if (candidate.length !== DEMO_CODE.length) return;
setStatus("verifying");
if (timerRef.current) window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
if (candidate === DEMO_CODE) {
setStatus("success");
} else {
setStatus("error");
if (!reduce) setShake(true);
}
}, 850);
},
[reduce]
);
const handleCodeChange = (next: string) => {
setCode(next);
if (status === "error" || status === "success") setStatus("idle");
};
const resetCode = () => {
setCode("");
setStatus("idle");
};
const handleResend = () => {
setSeconds(29);
resetCode();
};
const codeReady = digitsOf(code).length === DEMO_CODE.length;
const codeTone: Tone = status === "error" ? "error" : status === "success" ? "success" : "default";
const codeLocked = status === "verifying" || status === "success";
const liveMessage =
status === "verifying"
? "Verifying your code."
: status === "success"
? "Code verified. You are signed in."
: status === "error"
? "That code is incorrect. Please try again."
: "";
/* --- transaction PIN card --- */
const [pin, setPin] = useState("");
const [reveal, setReveal] = useState(false);
const [pinAuthorized, setPinAuthorized] = useState<null | boolean>(null);
const handlePinChange = (next: string) => {
setPin(next);
if (pinAuthorized !== null) setPinAuthorized(null);
};
const handlePinComplete = (candidate: string) => {
setPinAuthorized(candidate === DEMO_PIN);
};
const pinReady = digitsOf(pin).length === DEMO_PIN.length;
const pinTone: Tone = pinAuthorized === false ? "error" : pinAuthorized ? "success" : "default";
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-24">
<style>{`
@keyframes otp6-shake {
10%, 90% { transform: translateX(-1px); }
20%, 80% { transform: translateX(2px); }
30%, 50%, 70% { transform: translateX(-4px); }
40%, 60% { transform: translateX(4px); }
}
@keyframes otp6-caret {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@keyframes otp6-draw { to { stroke-dashoffset: 0; } }
.otp6-shake { animation: otp6-shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
.otp6-caret-el { animation: otp6-caret 1.1s steps(1) infinite; }
.otp6-check { animation: otp6-draw 0.5s 0.05s ease forwards; }
@media (prefers-reduced-motion: reduce) {
.otp6-shake, .otp6-caret-el, .otp6-check { animation: none !important; }
}
`}</style>
{/* atmosphere */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20" />
<div className="absolute -bottom-24 right-10 h-64 w-64 rounded-full bg-violet-300/25 blur-3xl dark:bg-violet-700/15" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(15,23,42,0.06)_1px,transparent_0)] bg-[size:22px_22px] dark:bg-[radial-gradient(circle_at_1px_1px,rgba(148,163,184,0.08)_1px,transparent_0)]" />
</div>
<div className="relative mx-auto max-w-5xl">
<header className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-widest text-indigo-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-indigo-300">
One-time code
</span>
<h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
Confirm it’s really you
</h2>
<p className="mx-auto mt-3 max-w-lg text-pretty text-sm leading-relaxed text-slate-600 dark:text-slate-400 sm:text-base">
Accessible six-box verification with auto-advance, paste-anywhere, and full keyboard
control. Try typing, or paste a code — it splits across every box.
</p>
</header>
<div className="mt-12 grid gap-6 lg:grid-cols-5">
{/* ---------- Email verification ---------- */}
<div className="lg:col-span-3">
<div className="h-full rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/70 sm:p-8">
<div className="flex items-center gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-indigo-100 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300">
<EnvelopeIcon />
</span>
<div>
<h3 className="text-base font-semibold">Verify your email</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">
Code sent to <span className="font-medium text-slate-700 dark:text-slate-200">sardar@webinnoventix.com</span>
</p>
</div>
</div>
<div className="mt-7">
<div className="mb-3 flex items-baseline justify-between">
<span id={hintId} className="text-sm font-medium text-slate-700 dark:text-slate-300">
Enter your 6-digit code
</span>
<span className="text-xs text-slate-400 dark:text-slate-500">
Demo code: <span className="font-mono font-semibold text-slate-500 dark:text-slate-400">482913</span>
</span>
</div>
<OtpField
length={6}
value={code}
onChange={handleCodeChange}
onComplete={runVerify}
tone={codeTone}
disabled={codeLocked}
shake={shake}
onShakeEnd={() => setShake(false)}
ariaLabel="Email verification code"
describedById={hintId}
/>
<p aria-live="polite" className="sr-only">
{liveMessage}
</p>
{/* status region */}
<div className="mt-5 min-h-[3.25rem]">
<AnimatePresence mode="wait" initial={false}>
{status === "verifying" ? (
<motion.div
key="verifying"
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
transition={{ duration: reduce ? 0 : 0.2 }}
className="flex items-center gap-2 rounded-xl bg-slate-100 px-4 py-3 text-sm font-medium text-slate-600 dark:bg-slate-800/70 dark:text-slate-300"
>
<SpinnerIcon reduce={reduce} />
Verifying your code…
</motion.div>
) : status === "success" ? (
<motion.div
key="success"
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
transition={{ duration: reduce ? 0 : 0.2 }}
className="flex items-center gap-2 rounded-xl bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"
>
<CheckDrawIcon reduce={reduce} />
Verified — welcome back, Sardar.
</motion.div>
) : status === "error" ? (
<motion.div
key="error"
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
transition={{ duration: reduce ? 0 : 0.2 }}
className="flex items-center gap-2 rounded-xl bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700 dark:bg-rose-500/10 dark:text-rose-300"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" className="h-5 w-5" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M12 7v6M12 16.5v.5" />
</svg>
That code doesn’t match. Check the digits and try again.
</motion.div>
) : null}
</AnimatePresence>
</div>
{/* actions */}
<div className="mt-2 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => runVerify(digitsOf(code))}
disabled={!codeReady || codeLocked}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 dark:focus-visible:ring-offset-slate-900 dark:disabled:bg-slate-700 dark:disabled:text-slate-400"
>
{status === "success" ? "Continue" : "Verify code"}
</button>
<button
type="button"
onClick={resetCode}
className="inline-flex items-center justify-center rounded-xl px-4 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Clear
</button>
</div>
<div className="mt-6 border-t border-slate-200 pt-4 text-sm dark:border-slate-800">
<span className="text-slate-500 dark:text-slate-400">Didn’t get the email? </span>
{seconds > 0 ? (
<span className="font-medium text-slate-400 dark:text-slate-500">
Resend in {formatCountdown(seconds)}
</span>
) : (
<button
type="button"
onClick={handleResend}
className="rounded font-semibold text-indigo-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-300 dark:focus-visible:ring-offset-slate-900"
>
Resend code
</button>
)}
</div>
</div>
</div>
</div>
{/* ---------- Transaction PIN (masked) ---------- */}
<div className="lg:col-span-2">
<div className="h-full rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/70 sm:p-8">
<div className="flex items-center gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-300">
<ShieldIcon />
</span>
<div>
<h3 className="text-base font-semibold">Authorize payment</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">Masked 4-digit PIN</p>
</div>
</div>
<p id={pinHintId} className="mt-6 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
Enter your PIN to send{" "}
<span className="font-semibold text-slate-800 dark:text-slate-200">$1,240.00</span> to Acme Studio.
</p>
<div className="mt-5 flex items-center justify-between">
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">Transaction PIN</span>
<button
type="button"
onClick={() => setReveal((r) => !r)}
aria-pressed={reveal}
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs font-medium text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900"
>
{reveal ? <EyeOffIcon /> : <EyeIcon />}
{reveal ? "Hide" : "Show"}
</button>
</div>
<div className="mt-3">
<OtpField
length={4}
value={pin}
onChange={handlePinChange}
onComplete={handlePinComplete}
mask
reveal={reveal}
tone={pinTone}
ariaLabel="Transaction PIN"
describedById={pinHintId}
/>
</div>
<div className="mt-4 min-h-[1.5rem] text-sm" aria-live="polite">
{pinAuthorized === true ? (
<span className="font-medium text-emerald-600 dark:text-emerald-400">Payment authorized.</span>
) : pinAuthorized === false ? (
<span className="font-medium text-rose-600 dark:text-rose-400">Incorrect PIN. Try 7391.</span>
) : (
<span className="text-slate-400 dark:text-slate-500">Demo PIN: 7391</span>
)}
</div>
<button
type="button"
disabled={!pinReady || pinAuthorized === true}
onClick={() => handlePinComplete(digitsOf(pin))}
className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 dark:focus-visible:ring-offset-slate-900 dark:disabled:bg-slate-700 dark:disabled:text-slate-400"
>
{pinAuthorized ? "Authorized" : "Authorize transfer"}
</button>
<p className="mt-4 flex items-center gap-1.5 text-xs text-slate-400 dark:text-slate-500">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} className="h-3.5 w-3.5" aria-hidden="true">
<rect x="5" y="11" width="14" height="9" rx="2" />
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
</svg>
Never share your PIN. We’ll never ask for it by phone.
</p>
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Set Input
Originaldefault, hover, focus, disabled, error text inputs

Floating Label Input
Originallabel that floats up on focus/fill

Filled Input
Originalfilled material-style inputs

Underline Input
Originalminimal underline inputs with animated bar

Search Input
Originalsearch box with icon and clear

Search Suggest Input
Originalsearch with a live suggestions dropdown

Password Toggle Input
Originalpassword field with show/hide toggle

Textarea Autosize Input
Originaltextarea that grows with content

Addon Group Input
Originalinput group with leading/trailing addons

Currency Input
Originalcurrency input with formatting

Tags Input
Originaltag input, add and remove chips

Phone Input
Originalphone input with country prefix select

