Banking — OTP / 2FA Input
A trust-first six-digit OTP and two-factor verification screen for banking and fintech flows. Boxes auto-advance as you type, accept a full pasted code, and support an optional masked mode for shoulder-surfing safety. A resend countdown timer throttles new codes, while clear error and success states confirm whether the entered code matches. Includes an encrypted badge, accessible inputs, keyboard navigation, tabular figures, and a responsive layout that holds together down to small mobile widths.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.1);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06);
--sh-2: 0 8px 24px rgba(14, 27, 58, 0.1);
--sh-3: 0 24px 60px rgba(14, 27, 58, 0.16);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-variant-numeric: tabular-nums;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 80% -10%, rgba(59, 110, 246, 0.1), transparent 60%),
radial-gradient(100% 70% at -10% 110%, rgba(124, 92, 255, 0.08), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 18px;
}
/* ---------- card ---------- */
.card {
width: 100%;
max-width: 432px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
overflow: hidden;
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 22px;
background: linear-gradient(180deg, var(--navy), var(--navy-2));
color: #fff;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand__mark {
display: inline-grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.12);
color: #cfe0ff;
}
.brand__name {
font-size: 15px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
padding: 5px 10px;
border-radius: 999px;
white-space: nowrap;
}
.pill--secure {
background: rgba(15, 181, 166, 0.16);
color: #8af0e5;
border: 1px solid rgba(15, 181, 166, 0.35);
}
.card__body {
padding: 26px 26px 22px;
}
.step {
display: inline-block;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--accent-d);
background: var(--accent-50);
padding: 4px 10px;
border-radius: 999px;
}
.title {
margin: 14px 0 8px;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--ink);
}
.lede {
margin: 0 0 22px;
font-size: 14px;
color: var(--muted);
}
.lede strong {
color: var(--ink);
font-variant-numeric: tabular-nums;
}
/* ---------- otp ---------- */
.otp {
border: 0;
margin: 0;
padding: 0;
}
.otp__boxes {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
}
.otp__sep {
width: 12px;
height: 2px;
border-radius: 2px;
background: var(--line-2);
flex: none;
}
.otp__box {
flex: 1 1 0;
min-width: 0;
height: 58px;
text-align: center;
font-family: inherit;
font-variant-numeric: tabular-nums;
font-size: 24px;
font-weight: 700;
color: var(--ink);
background: #fbfcfe;
border: 1.5px solid var(--line-2);
border-radius: var(--r-md);
caret-color: var(--accent);
transition:
border-color 0.16s ease,
box-shadow 0.16s ease,
background 0.16s ease,
transform 0.08s ease;
}
.otp__box:hover {
border-color: var(--accent);
}
.otp__box:focus {
outline: none;
border-color: var(--accent);
background: #fff;
box-shadow: 0 0 0 4px rgba(59, 110, 246, 0.16);
}
.otp__box.is-filled {
background: #fff;
border-color: rgba(59, 110, 246, 0.55);
}
.otp__box.is-mask {
font-family: "Inter", sans-serif;
font-size: 30px;
}
/* error state */
.otp.is-error .otp__box {
border-color: var(--danger);
background: #fff6f5;
animation: shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97);
}
.otp.is-error .otp__box:focus {
box-shadow: 0 0 0 4px rgba(212, 73, 62, 0.16);
}
/* success state */
.otp.is-success .otp__box {
border-color: var(--ok);
background: #f1fbf5;
color: var(--ok);
}
@keyframes shake {
10%,
90% {
transform: translateX(-1px);
}
20%,
80% {
transform: translateX(2px);
}
30%,
50%,
70% {
transform: translateX(-4px);
}
40%,
60% {
transform: translateX(4px);
}
}
.otp__status {
min-height: 18px;
margin: 12px 2px 0;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.otp__status.is-error {
color: var(--danger);
}
.otp__status.is-success {
color: var(--ok);
}
/* ---------- row / toggle / resend ---------- */
.row {
display: flex;
align-items: center;
gap: 12px;
margin: 18px 0;
}
.row--between {
justify-content: space-between;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 9px;
cursor: pointer;
user-select: none;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
}
.toggle input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.toggle__track {
position: relative;
width: 38px;
height: 22px;
border-radius: 999px;
background: var(--line-2);
transition: background 0.18s ease;
flex: none;
}
.toggle__dot {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-1);
transition: transform 0.18s ease;
}
.toggle input:checked + .toggle__track {
background: var(--accent);
}
.toggle input:checked + .toggle__track .toggle__dot {
transform: translateX(16px);
}
.toggle input:focus-visible + .toggle__track {
box-shadow: 0 0 0 3px rgba(59, 110, 246, 0.3);
}
.resend {
appearance: none;
border: 0;
background: transparent;
font-family: inherit;
font-size: 13px;
font-weight: 600;
color: var(--muted);
cursor: not-allowed;
padding: 6px 4px;
border-radius: var(--r-sm);
}
.resend:not(:disabled) {
color: var(--accent-d);
cursor: pointer;
}
.resend:not(:disabled):hover {
text-decoration: underline;
}
.resend:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---------- buttons ---------- */
.btn {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
font-family: inherit;
font-size: 15px;
font-weight: 700;
border: 0;
border-radius: var(--r-md);
padding: 14px 18px;
cursor: pointer;
transition:
transform 0.08s ease,
box-shadow 0.18s ease,
background 0.18s ease,
opacity 0.18s ease;
}
.btn--primary {
color: #fff;
background: linear-gradient(180deg, var(--accent), var(--accent-d));
box-shadow: 0 8px 20px rgba(43, 85, 204, 0.32);
}
.btn--primary:hover {
box-shadow: 0 12px 26px rgba(43, 85, 204, 0.4);
}
.btn--primary:active {
transform: translateY(1px) scale(0.995);
}
.btn:focus-visible {
outline: 2px solid var(--accent-d);
outline-offset: 2px;
}
.btn.is-loading {
pointer-events: none;
opacity: 0.9;
}
.btn.is-loading .btn__label {
opacity: 0.65;
}
.btn__spin {
display: none;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.45);
border-top-color: #fff;
animation: spin 0.7s linear infinite;
}
.btn.is-loading .btn__spin {
display: inline-block;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.btn--primary.is-done {
background: linear-gradient(180deg, #25b06f, var(--ok));
box-shadow: 0 8px 20px rgba(31, 157, 98, 0.32);
}
/* ---------- hint / footer ---------- */
.hint {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 18px 0 0;
font-size: 12.5px;
color: var(--muted);
background: #f7f9fd;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 11px 13px;
}
.hint svg {
flex: none;
margin-top: 1px;
color: var(--accent);
}
.hint strong {
color: var(--ink);
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
.card__foot {
display: flex;
align-items: center;
gap: 9px;
padding: 14px 26px;
font-size: 12.5px;
color: var(--muted);
background: #fafbfe;
border-top: 1px solid var(--line);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
}
.dot--ok {
background: var(--ok);
box-shadow: 0 0 0 3px rgba(31, 157, 98, 0.18);
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--navy);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 12px 18px;
border-radius: 999px;
box-shadow: var(--sh-3);
opacity: 0;
pointer-events: none;
transition:
opacity 0.22s ease,
transform 0.22s ease;
z-index: 50;
max-width: 90vw;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast.is-error {
background: var(--danger);
}
.toast.is-success {
background: var(--ok);
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.page {
padding: 18px 12px;
}
.card__body {
padding: 22px 18px 18px;
}
.card__head,
.card__foot {
padding-left: 18px;
padding-right: 18px;
}
.otp__boxes {
gap: 7px;
}
.otp__box {
height: 52px;
font-size: 21px;
border-radius: 11px;
}
.otp__sep {
width: 7px;
}
.title {
font-size: 20px;
}
}
@media (max-width: 360px) {
.otp__sep {
display: none;
}
}(function () {
"use strict";
var VALID_CODE = "481902";
var RESEND_SECONDS = 30;
var form = document.getElementById("otp-form");
var boxesWrap = document.getElementById("otp-boxes");
var boxes = Array.prototype.slice.call(
boxesWrap.querySelectorAll(".otp__box")
);
var fieldset = boxesWrap.closest(".otp");
var status = document.getElementById("otp-status");
var maskToggle = document.getElementById("mask-toggle");
var verifyBtn = document.getElementById("verify-btn");
var resendBtn = document.getElementById("resend-btn");
var resendText = document.getElementById("resend-text");
var resendCount = document.getElementById("resend-count");
var toastEl = document.getElementById("toast");
var masked = false;
var locked = false;
var toastTimer;
var resendTimer;
/* ---------- toast helper ---------- */
function toast(msg, kind) {
toastEl.textContent = msg;
toastEl.className = "toast is-show" + (kind ? " is-" + kind : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.className = "toast";
}, 2600);
}
/* ---------- digits / display ---------- */
function getCode() {
return boxes
.map(function (b) {
return b.dataset.value || "";
})
.join("");
}
function render(box) {
var v = box.dataset.value || "";
box.value = v ? (masked && v ? "•" : v) : "";
box.classList.toggle("is-filled", !!v);
box.classList.toggle("is-mask", masked && !!v);
}
function renderAll() {
boxes.forEach(render);
}
function setDigit(box, digit) {
box.dataset.value = digit || "";
render(box);
}
function clearStates() {
fieldset.classList.remove("is-error", "is-success");
status.className = "otp__status";
status.textContent = "";
}
function focusBox(i) {
if (i >= 0 && i < boxes.length) {
boxes[i].focus();
boxes[i].select();
}
}
function firstEmptyIndex() {
for (var i = 0; i < boxes.length; i++) {
if (!boxes[i].dataset.value) return i;
}
return boxes.length - 1;
}
/* ---------- input handling ---------- */
boxes.forEach(function (box, i) {
box.addEventListener("input", function () {
if (locked) {
render(box);
return;
}
clearStates();
var raw = box.value.replace(/\D/g, "");
if (!raw) {
setDigit(box, "");
return;
}
// take last typed digit (handles overwrite)
var digit = raw.slice(-1);
setDigit(box, digit);
if (i < boxes.length - 1) focusBox(i + 1);
maybeAutoSubmit();
});
box.addEventListener("keydown", function (e) {
if (locked) {
e.preventDefault();
return;
}
if (e.key === "Backspace") {
if (box.dataset.value) {
setDigit(box, "");
} else if (i > 0) {
setDigit(boxes[i - 1], "");
focusBox(i - 1);
}
e.preventDefault();
clearStates();
} else if (e.key === "ArrowLeft") {
e.preventDefault();
focusBox(i - 1);
} else if (e.key === "ArrowRight") {
e.preventDefault();
focusBox(i + 1);
}
});
box.addEventListener("focus", function () {
box.select();
});
});
/* ---------- paste support ---------- */
boxesWrap.addEventListener("paste", function (e) {
if (locked) return;
e.preventDefault();
var text = (e.clipboardData || window.clipboardData).getData("text") || "";
var digits = text.replace(/\D/g, "").slice(0, boxes.length);
if (!digits) return;
clearStates();
boxes.forEach(function (b, idx) {
setDigit(b, digits[idx] || "");
});
var next = digits.length >= boxes.length ? boxes.length - 1 : digits.length;
focusBox(next);
toast("Code pasted from clipboard", "");
maybeAutoSubmit();
});
function maybeAutoSubmit() {
if (getCode().length === boxes.length) {
// tiny delay so the last digit renders before validating
setTimeout(function () {
if (getCode().length === boxes.length && !locked) verify();
}, 120);
}
}
/* ---------- mask toggle ---------- */
maskToggle.addEventListener("change", function () {
masked = maskToggle.checked;
renderAll();
});
/* ---------- verify ---------- */
function verify() {
var code = getCode();
if (code.length < boxes.length) {
fieldset.classList.add("is-error");
status.className = "otp__status is-error";
status.textContent = "Please enter all 6 digits.";
focusBox(firstEmptyIndex());
return;
}
verifyBtn.classList.add("is-loading");
locked = true;
setTimeout(function () {
verifyBtn.classList.remove("is-loading");
if (code === VALID_CODE) {
fieldset.classList.add("is-success");
status.className = "otp__status is-success";
status.textContent = "Verified — your transfer is confirmed.";
verifyBtn.classList.add("is-done");
verifyBtn.querySelector(".btn__label").textContent = "Verified";
toast("Identity verified successfully", "success");
stopResend();
resendBtn.disabled = true;
resendText.textContent = "Verified";
} else {
locked = false;
fieldset.classList.add("is-error");
status.className = "otp__status is-error";
status.textContent = "That code isn't right. Check and try again.";
toast("Incorrect verification code", "error");
// clear and refocus
boxes.forEach(function (b) {
setDigit(b, "");
});
focusBox(0);
}
}, 850);
}
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!locked) verify();
});
/* ---------- resend countdown ---------- */
function startResend() {
var remaining = RESEND_SECONDS;
resendBtn.disabled = true;
resendCount.textContent = remaining;
resendText.innerHTML =
'Resend in <span id="resend-count">' + remaining + "</span>s";
resendTimer = setInterval(function () {
remaining -= 1;
if (remaining <= 0) {
stopResend();
resendBtn.disabled = false;
resendText.textContent = "Resend code";
return;
}
var c = document.getElementById("resend-count");
if (c) c.textContent = remaining;
}, 1000);
}
function stopResend() {
clearInterval(resendTimer);
}
resendBtn.addEventListener("click", function () {
if (resendBtn.disabled || locked) return;
boxes.forEach(function (b) {
setDigit(b, "");
});
clearStates();
focusBox(0);
toast("A new code was sent to •••• 4291", "success");
startResend();
});
/* ---------- init ---------- */
renderAll();
startResend();
focusBox(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — OTP / 2FA Input</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<section class="card" aria-labelledby="otp-title">
<header class="card__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path
d="M12 2 4 6v5c0 4.7 3.2 8.8 8 10 4.8-1.2 8-5.3 8-10V6l-8-4Z"
fill="currentColor"
opacity=".18"
/>
<path
d="M12 2 4 6v5c0 4.7 3.2 8.8 8 10 4.8-1.2 8-5.3 8-10V6l-8-4Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
<path
d="m8.5 12 2.4 2.4 4.6-4.8"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span class="brand__name">Northvale Bank</span>
</div>
<span class="pill pill--secure">
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true">
<path
d="M7 10V8a5 5 0 0 1 10 0v2m-9 0h8a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Encrypted
</span>
</header>
<div class="card__body">
<span class="step" aria-hidden="true">Step 2 of 2 · Verify</span>
<h1 id="otp-title" class="title">Enter your security code</h1>
<p class="lede">
We sent a 6-digit verification code to your registered device
<strong>(•••• 4291)</strong>. The code expires shortly — enter it to
confirm the transfer.
</p>
<form id="otp-form" novalidate>
<fieldset class="otp" aria-describedby="otp-help otp-status">
<legend class="sr-only">6-digit one-time passcode</legend>
<div
class="otp__boxes"
id="otp-boxes"
role="group"
aria-label="One-time passcode, 6 digits"
>
<input
class="otp__box"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="1"
aria-label="Digit 1"
data-index="0"
/>
<input
class="otp__box"
inputmode="numeric"
maxlength="1"
aria-label="Digit 2"
data-index="1"
/>
<input
class="otp__box"
inputmode="numeric"
maxlength="1"
aria-label="Digit 3"
data-index="2"
/>
<span class="otp__sep" aria-hidden="true"></span>
<input
class="otp__box"
inputmode="numeric"
maxlength="1"
aria-label="Digit 4"
data-index="3"
/>
<input
class="otp__box"
inputmode="numeric"
maxlength="1"
aria-label="Digit 5"
data-index="4"
/>
<input
class="otp__box"
inputmode="numeric"
maxlength="1"
aria-label="Digit 6"
data-index="5"
/>
</div>
<p id="otp-status" class="otp__status" role="status" aria-live="polite"></p>
</fieldset>
<div class="row row--between">
<label class="toggle">
<input type="checkbox" id="mask-toggle" />
<span class="toggle__track" aria-hidden="true"
><span class="toggle__dot"></span
></span>
<span class="toggle__label">Mask digits</span>
</label>
<button type="button" class="resend" id="resend-btn" disabled>
<span id="resend-text">Resend in <span id="resend-count">30</span>s</span>
</button>
</div>
<button type="submit" class="btn btn--primary" id="verify-btn">
<span class="btn__label">Verify & continue</span>
<span class="btn__spin" aria-hidden="true"></span>
</button>
<p id="otp-help" class="hint">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.6" />
<path d="M12 11v5M12 8h.01" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
Demo code is <strong>481902</strong>. Never share a real code with anyone — not even Northvale staff.
</p>
</form>
</div>
<footer class="card__foot">
<span class="dot dot--ok" aria-hidden="true"></span>
Two-factor authentication keeps your account secure.
</footer>
</section>
</main>
<div id="toast" class="toast" role="alert" aria-live="assertive"></div>
<script src="script.js"></script>
</body>
</html>OTP / 2FA Input
A focused verification screen for confirming a sensitive banking action, styled with a calm navy-and-trust-blue palette. Six single-character boxes form the one-time passcode field: typing a digit advances the caret to the next box, Backspace steps backward and clears, and the arrow keys move between positions. Pasting a code from an SMS or authenticator app fills every box at once and triggers validation automatically.
A “Mask digits” toggle swaps the entered characters for dots so the code can’t be read over your shoulder, and a resend control stays disabled behind a 30-second countdown to discourage rapid retries. When you submit, the screen shows a brief loading state, then resolves into either a red error state — with a shake animation and a cleared field — or a green verified state confirming the transfer.
Everything runs on vanilla JavaScript with a small toast() helper, accessible aria-live status messaging, tabular figures for the code, WCAG-AA contrast, and a responsive layout tuned for narrow mobile screens. The demo code is 481902.
Illustrative UI only — not real banking software or financial advice.