Verification code input
Category: Authentication · Tags: otp, one-time-code, two-factor, verification, resend
How to use it
Use for a single six-digit code sent by email or text message during sign-in or verification. Prefer this one input over six separate boxes, which break paste and autofill. The handlers only show demo messages: the developer must wire Verify and Resend to the real endpoints, set the pattern and maxlength to the real code length, change the 30-second cooldown if the server uses another, and rename the dcl-otp ids if it appears twice.
Accessibility
One native text input with inputmode="numeric" (number keypad on phones), autocomplete="one-time-code" (the code can be filled from a text message) and pattern="[0-9]{6}", labelled with <label for> and described by the instruction through aria-describedby. Resend is a native <button> that uses aria-disabled during the cooldown, so it stays focusable and its countdown text stays readable; results go to a role="status" region. Text #0f172a on white is 17.85:1, the hint #475569 on white is 7.58:1, the resend text #1d4ed8 on white is 6.70:1 and #475569 on #f1f5f9 during the cooldown is 6.92:1, and the verify button's white on #1d4ed8 (#1e40af hovered) is 6.70:1 (8.72:1).
Source (copy-ready)
<form class="dcl-otp" aria-labelledby="dcl-otp-title">
<h2 id="dcl-otp-title" class="dcl-otp__title">Enter your code</h2>
<label for="dcl-otp-code" class="dcl-otp__label">Verification code</label>
<p id="dcl-otp-hint" class="dcl-otp__hint">We sent a 6-digit code to your email. It expires in 10 minutes.</p>
<input type="text" id="dcl-otp-code" name="code" class="dcl-otp__input" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" required aria-describedby="dcl-otp-hint">
<button type="submit" class="dcl-otp__verify">Verify</button>
<div class="dcl-otp__resend-row">
<span class="dcl-otp__question">Did not get it?</span>
<button type="button" class="dcl-otp__resend" aria-disabled="true" data-cooldown="30">Resend code in 30 s</button>
</div>
<p class="dcl-otp__status" role="status"></p>
</form>
<style>
.dcl-otp { display: flex; flex-direction: column; gap: 0.6rem; max-width: 22rem; padding: 1.5rem; border: 1px solid #cbd5e1; border-radius: 0.75rem; font: inherit; color: #0f172a; background-color: #ffffff; box-sizing: border-box; }
.dcl-otp__title { margin: 0; font-size: 1.375rem; color: #0f172a; background-color: #ffffff; }
.dcl-otp__label { font-weight: 600; color: #0f172a; background-color: #ffffff; }
.dcl-otp__hint { margin: 0; font-size: 0.9rem; color: #475569; background-color: #ffffff; }
.dcl-otp__input { width: 100%; max-width: 12rem; box-sizing: border-box; padding: 0.55rem 0.65rem; border: 1px solid #64748b; border-radius: 6px; font: inherit; font-size: 1.375rem; letter-spacing: 0.3em; font-variant-numeric: tabular-nums; color: #0f172a; background-color: #ffffff; }
.dcl-otp__input:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-otp__verify { padding: 0.65rem 1rem; border: none; border-radius: 6px; font: inherit; font-weight: 600; cursor: pointer; color: #ffffff; background-color: #1d4ed8; }
.dcl-otp__verify:hover { color: #ffffff; background-color: #1e40af; }
.dcl-otp__verify:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-otp__resend-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.dcl-otp__question { color: #334155; background-color: #ffffff; }
.dcl-otp__resend { padding: 0.35rem 0.6rem; border: 1px solid #94a3b8; border-radius: 6px; font: inherit; cursor: pointer; color: #1d4ed8; background-color: #ffffff; }
.dcl-otp__resend:hover { color: #1e40af; background-color: #eff6ff; }
.dcl-otp__resend:focus-visible { outline: 2px solid #1d4ed8; outline-offset: 2px; }
.dcl-otp__resend[aria-disabled="true"] { cursor: not-allowed; color: #475569; background-color: #f1f5f9; }
.dcl-otp__status { margin: 0; font-size: 0.9rem; color: #334155; background-color: #ffffff; }
</style>
<script>
(function () {
document.querySelectorAll(".dcl-otp").forEach(function (form) {
if (form.dataset.dclBound) {
return;
}
form.dataset.dclBound = "true";
var resend = form.querySelector(".dcl-otp__resend");
var status = form.querySelector(".dcl-otp__status");
var timer = null;
function say(text) {
if (status) {
status.textContent = text;
}
}
function startCooldown() {
if (!resend) {
return;
}
var left = parseInt(resend.getAttribute("data-cooldown"), 10) || 30;
resend.setAttribute("aria-disabled", "true");
resend.textContent = "Resend code in " + left + " s";
clearInterval(timer);
timer = setInterval(function () {
left -= 1;
if (left <= 0) {
clearInterval(timer);
resend.setAttribute("aria-disabled", "false");
resend.textContent = "Resend code";
return;
}
resend.textContent = "Resend code in " + left + " s";
}, 1000);
}
if (resend) {
resend.addEventListener("click", function () {
if (resend.getAttribute("aria-disabled") === "true") {
return;
}
say("A new code is on its way. (Demo only: nothing was sent.)");
startCooldown();
});
}
form.addEventListener("submit", function (event) {
event.preventDefault();
say("Demo only: the code was not checked. Connect this form to your verification endpoint.");
});
startCooldown();
});
})();
</script>
Was this useful? Send feedback.