One-time Code Input
A row of boxes for a verification code. Each digit springs into its box as you type and the focus hops along. Fill the last box and a wrong code shakes the row red before it clears, while the right one ripples green. Backspace, arrow keys and pasting a whole code all work.
The right code is 482916.
How to use it
- Paste the snippet just before
</head>. - Add an empty element with
data-one-time-code-input: the boxes are built inside it. - Set
data-codeto check the code in the page, or listen for thecompleteevent and check it yourself.
Options
Add any of these attributes to the same element.
data-code | The right code. Without it, every full code counts as right unless your complete handler calls animacious.oneTimeCode.error(el). |
data-length | Number of boxes. Default 6, or the length of data-code. |
data-name | Adds a hidden input with this name, so the code posts with a form. |
data-autoplay | It types a wrong code, then the right one, over and over while in view. It stops as soon as you use it. |
Phones can fill it from the code in a text message (the first box has autocomplete="one-time-code"). animacious.oneTimeCode.reset(el) empties it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// One-time Code Input: a row of boxes for a verification code. Each digit
// springs into its box as you type and the focus hops along; backspace,
// arrow keys and pasting a whole code all work. Fill the last box and the
// code is checked: a wrong one shakes the row and flashes red before it
// clears, a right one ripples green.
// Usage: <div data-one-time-code-input data-code="482916"></div>
// Optional: data-length (number of boxes, default 6; taken from data-code
// when that's set), data-code (the right code; without it every full code
// counts as right unless you call animacious.oneTimeCode.error(el) from your
// "complete" handler), data-name (a hidden input's name, so it posts with a
// form), data-autoplay (it types a wrong code, then the right one, over
// and over while in view; it stops as soon as you use it).
// Listen for the "complete" event: e.detail.code is the full code.
var DEFAULTS = { length: 6, demo: '482916' };
var RED = '#dc2626', GREEN = '#16a34a';
var css =
'.otc{display:flex;justify-content:center;gap:0.45em}' +
'.otc-box{position:relative;display:grid;place-items:center;box-sizing:border-box;width:1.9em;height:2.3em;margin:0;padding:0;' +
'border:0.07em solid #d4d4d8;border-radius:0.35em;background:#fff;color:inherit;font:inherit;font-weight:700;text-align:center;' +
'caret-color:transparent;outline:none;transition:border-color .2s,box-shadow .2s}' +
'.otc-box.otc-on{border-color:var(--otc-accent);box-shadow:0 0 0 0.12em var(--otc-ring)}' +
'.otc-digit{display:block;line-height:1}' +
'.otc-hidden{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
var d = el.dataset;
var code = (d.code || '').replace(/\D/g, '');
var n = code.length || parseInt(d.length, 10) || DEFAULTS.length;
var mock = !!el.closest('[aria-hidden="true"]');
el.classList.add('otc');
el.style.setProperty('--otc-accent', '#6a0dad');
el.style.setProperty('--otc-ring', 'rgba(106,13,173,0.15)');
if (!mock) {
el.setAttribute('role', 'group');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Verification code');
}
var boxes = [];
for (var i = 0; i < n; i++) {
var box;
if (mock) {
box = document.createElement('span');
} else {
box = document.createElement('input');
box.type = 'text';
box.inputMode = 'numeric';
box.maxLength = 1;
box.autocomplete = i === 0 ? 'one-time-code' : 'off';
box.setAttribute('aria-label', 'Digit ' + (i + 1) + ' of ' + n);
}
box.className = 'otc-box';
el.appendChild(box);
boxes.push(box);
}
var hidden = null;
if (d.name && !mock) {
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = d.name;
el.appendChild(hidden);
}
var digits = [];
var busy = false, done = false;
// Show one digit: it springs up into its box.
function show(i, ch) {
var box = boxes[i];
digits[i] = ch;
if (mock) {
box.innerHTML = ch ? '<span class="otc-digit">' + ch + '</span>' : '';
if (ch && !reduced()) anime.animate(box.firstChild, { scale: [0.3, 1], y: ['0.5em', '0em'], opacity: [0, 1], duration: 520, ease: 'outElastic(1, .6)' });
} else {
box.value = ch || '';
if (ch && !reduced()) {
anime.animate(box, { scale: [{ to: 1.12, duration: 110, ease: 'out(2)' }, { to: 1, duration: 420, ease: 'outElastic(1, .5)' }] });
}
}
if (ch && !reduced()) anime.animate(box, { y: [{ to: '-0.18em', duration: 120, ease: 'out(2)' }, { to: 0, duration: 380, ease: 'outBounce' }] });
if (hidden) hidden.value = digits.join('');
}
function highlight(i) {
boxes.forEach(function (b, j) { b.classList.toggle('otc-on', j === i); });
}
function paint(color) {
boxes.forEach(function (b) {
b.style.borderColor = color || '';
b.style.color = color || '';
});
}
function clearAll() {
for (var i = 0; i < n; i++) show(i, '');
paint('');
}
function check() {
var full = digits.join('');
if (full.length < n) return;
busy = true;
highlight(-1);
var ok = !code || full === code;
el._otcError = false;
el.dispatchEvent(new CustomEvent('complete', { bubbles: true, detail: { code: full } }));
if (el._otcError) ok = false;
if (ok) success(); else error();
}
function success() {
busy = done = true;
paint(GREEN);
if (reduced()) return;
anime.animate(boxes, {
y: [{ to: '-0.35em', duration: 180, ease: 'out(2)' }, { to: 0, duration: 520, ease: 'outBounce' }],
delay: anime.stagger(60)
});
anime.animate(boxes, { boxShadow: ['0 0 0 0em rgba(22,163,74,0.35)', '0 0 0 0.35em rgba(22,163,74,0)'], duration: 800, delay: anime.stagger(60, { start: 120 }), ease: 'out(2)' });
}
function error() {
busy = true;
paint(RED);
var reset = function () {
clearAll();
busy = false;
if (!mock && el.contains(document.activeElement)) boxes[0].focus();
};
if (reduced()) { setTimeout(reset, 900); return; }
var w = Math.max(6, el.offsetWidth * 0.03);
anime.animate(el, {
x: [{ to: -w }, { to: w * 0.8 }, { to: -w * 0.6 }, { to: w * 0.4 }, { to: -w * 0.2 }, { to: 0 }],
duration: 520,
ease: 'inOut(2)'
});
setTimeout(function () {
anime.animate(boxes, { opacity: [1, 0.3], duration: 200, delay: anime.stagger(30, { from: 'last' }), ease: 'in(2)' });
setTimeout(function () {
reset();
anime.utils.set(boxes, { opacity: 1 });
}, 200 + 30 * n);
}, 900);
}
// Typing into the real boxes.
if (!mock) {
boxes.forEach(function (box, i) {
box.addEventListener('focus', function () {
stop();
// After a right code, tapping a box starts again.
if (done) { done = busy = false; clearAll(); if (i) { boxes[0].focus(); return; } }
// Always fill from the first empty box.
var first = digits.slice(0, n).join('').length;
if (i > first && !digits[i]) { boxes[Math.min(first, n - 1)].focus(); return; }
highlight(i);
box.select();
});
box.addEventListener('blur', function () { setTimeout(function () { if (!el.contains(document.activeElement)) highlight(-1); }); });
box.addEventListener('input', function () {
var v = box.value.replace(/\D/g, '');
if (busy) { box.value = digits[i] || ''; return; }
if (v.length > 1) { fill(v, i); return; }
if (!v) { box.value = ''; digits[i] = ''; return; }
paint('');
show(i, v);
if (i < n - 1) boxes[i + 1].focus(); else check();
});
box.addEventListener('keydown', function (e) {
if (busy) return;
if (e.key === 'Backspace') {
e.preventDefault();
if (digits[i]) show(i, '');
else if (i > 0) { show(i - 1, ''); boxes[i - 1].focus(); }
} else if (e.key === 'ArrowLeft' && i > 0) { e.preventDefault(); boxes[i - 1].focus(); }
else if (e.key === 'ArrowRight' && i < n - 1 && digits[i]) { e.preventDefault(); boxes[i + 1].focus(); }
});
box.addEventListener('paste', function (e) {
e.preventDefault();
if (busy) return;
fill((e.clipboardData || window.clipboardData).getData('text'), i);
});
});
}
// Several digits at once (a paste, or the phone's code suggestion).
function fill(str, from) {
var v = String(str).replace(/\D/g, '').slice(0, n - from).split('');
paint('');
v.forEach(function (ch, k) { setTimeout(function () { show(from + k, ch); }, reduced() ? 0 : k * 60); });
setTimeout(function () {
var next = from + v.length;
if (next < n) boxes[next].focus();
else { if (!mock) boxes[n - 1].blur(); check(); }
}, reduced() ? 0 : v.length * 60 + 80);
}
// Autoplay: a wrong code, then the right one.
var timer = null, stopped = false;
function stop() { stopped = true; clearTimeout(timer); }
if (on(d.autoplay) && !reduced()) {
var right = code || (DEFAULTS.demo + '0000000000').slice(0, n);
var wrong = right.split('').map(function (c, k) { return k === n - 2 ? String((+c + 3) % 10) : c; }).join('');
var steps = [];
var type = function (str) {
str.split('').forEach(function (ch, k) {
steps.push([function () { highlight(k); show(k, ch); if (k === n - 1) check(); }, k === n - 1 ? 2000 : 200 + Math.random() * 120]);
});
};
steps.push([function () { busy = done = false; clearAll(); highlight(0); }, 700]);
type(wrong);
steps.push([function () { highlight(0); busy = false; }, 600]);
type(right);
steps.push([function () { highlight(-1); }, 1200]);
var run = function (k) {
if (stopped) return;
steps[k][0]();
timer = setTimeout(function () { run((k + 1) % steps.length); }, steps[k][1]);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (stopped) return;
if (entries[0].isIntersecting) timer = setTimeout(function () { run(0); }, 500);
}).observe(el);
}
el._otcReset = function () { busy = done = false; clearAll(); };
}
function init() {
document.querySelectorAll('[data-one-time-code-input]').forEach(function (el) {
if (el._otc) return;
el._otc = true;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.oneTimeCode = {
// Call from a "complete" handler to treat the code as wrong.
error: function (el) { el._otcError = true; },
reset: function (el) { if (el._otcReset) el._otcReset(); }
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-one-time-code-input data-length="6" data-name="code"></div>
<script>
document.querySelector('[data-one-time-code-input]').addEventListener('complete', function (e) {
if (e.detail.code !== '482916') animacious.oneTimeCode.error(e.target);
});
</script>