OTP Input
Overview
A six-box verification code input: auto-advances while typing, steps back on backspace and spreads a pasted code across the boxes. Validates when full: shakes on error, turns green when correct.
Plain HTML, CSS and a little vanilla JS, with no framework dependency. Copy the prompt into Claude Code, Cursor, Trae or Codex and the model adds OTP Input to your project in your colors.
Where to use it
Search boxes, AI chat inputs, form fields, file uploads, verification codes and anywhere else people type.
Install
With Builder, install it with the 8pt CLI or call it directly from an editor connected to 8pt MCP.
$ npm i -g @8pt-dev/cli && 8pt add otp-input
mcp › Use 8pt MCP to call get_component("otp-input")
Code
Source for OTP Input. Motion uses only transform and opacity and respects the Reduce motion setting.
<form class="x-otp" autocomplete="one-time-code">
<b>Enter the code</b>
<small>Sent to a•••@example.com · Sample code 246810</small>
<div class="x-otp__row">
<input inputmode="numeric" maxlength="1" aria-label="Digit 1"><input inputmode="numeric" maxlength="1" aria-label="Digit 2"><input inputmode="numeric" maxlength="1" aria-label="Digit 3"><span aria-hidden="true">–</span><input inputmode="numeric" maxlength="1" aria-label="Digit 4"><input inputmode="numeric" maxlength="1" aria-label="Digit 5"><input inputmode="numeric" maxlength="1" aria-label="Digit 6">
</div>
<p class="x-otp__msg" aria-live="polite"></p>
</form>.x-otp{display:grid;justify-items:center;gap:8px;color:var(--d-fg,#fff);text-align:center}
.x-otp b{font:650 16px/1.2 var(--d-font,system-ui,sans-serif)}
.x-otp small{font:12px/1.3 var(--d-font,system-ui,sans-serif);color:var(--d-muted,#9b9ba8)}
.x-otp__row{display:flex;align-items:center;gap:6px;margin-top:8px}
.x-otp__row span{color:var(--d-muted,#9b9ba8)}
.x-otp input{width:38px;height:46px;padding:0;border-radius:10px;border:1.5px solid var(--d-line,rgba(255,255,255,.16));background:var(--d-card,#14141c);color:inherit;font:700 20px/1 ui-monospace,monospace;text-align:center;outline:0;caret-color:var(--d-accent,#4D74FF);transition:border-color .2s,transform .2s}
.x-otp input:focus{border-color:var(--d-accent,#4D74FF);transform:translateY(-2px)}
.x-otp input.fill{border-color:color-mix(in srgb,var(--d-fg,#fff) 40%,transparent)}
.x-otp.ok input{border-color:#3dd68c;color:#3dd68c}
.x-otp.bad .x-otp__row{animation:x-otp-shake .4s}
.x-otp.bad input{border-color:#ff6b6b}
@keyframes x-otp-shake{20%,60%{transform:translateX(-6px)}40%,80%{transform:translateX(6px)}}
…const init = (root) => {
const form = root.querySelector('.x-otp'), ins = [...form.querySelectorAll('input')], msg = form.querySelector('.x-otp__msg');
const CODE = '246810';
let t;
const check = () => { const v = ins.map(i => i.value).join(''); form.classList.remove('ok', 'bad');
if (v.length < 6) { msg.textContent = ''; return; }
void form.offsetWidth;
if (v === CODE) { form.classList.add('ok'); msg.textContent = '✓ Verified'; }
else { form.classList.add('bad'); msg.textContent = 'That code isn’t right'; clearTimeout(t); t = setTimeout(() => { ins.forEach(i => { i.value = ''; i.classList.remove('fill'); }); form.classList.remove('bad'); msg.textContent = ''; ins[0].focus(); }, 900); } };
ins.forEach((inp, i) => {
inp.addEventListener('input', () => { inp.value = inp.value.replace(/\D/g, '').slice(-1); inp.classList.toggle('fill', !!inp.value); if (inp.value && ins[i + 1]) ins[i + 1].focus(); check(); });
inp.addEventListener('keydown', e => { if (e.key === 'Backspace' && !inp.value && ins[i - 1]) { ins[i - 1].focus(); ins[i - 1].value = ''; ins[i - 1].classList.remove('fill'); check(); }
…Subscribe to Builder to see the full source.See pricing