OTP input

OTP 입력

A fixed-length field built to fill a short verification code — usually from a text message — as fast as possible.

Also known as: One-time code inputPIN inputVerification code input
···
html
<div class="otw">
  <span class="otlabel">인증번호 6자리</span>
  <div class="otgroup" role="group" aria-label="인증번호 입력">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="1번째 자리">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="2번째 자리">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="3번째 자리">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="4번째 자리">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="5번째 자리">
    <input class="otcell" maxlength="1" inputmode="numeric" aria-label="6번째 자리">
  </div>
  <p class="otstatus" id="otstatus">문자로 받은 6자리를 입력하세요</p>
</div>
css
.otw{position:absolute;inset:0;display:grid;place-items:center;gap:10px}
.otlabel{color:var(--muted);font-size:10px}
.otgroup{display:flex;gap:6px}
.otcell{width:26px;height:32px;text-align:center;font-size:15px;font-weight:700;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--fg)}
.otcell[data-filled]{border-color:var(--accent);color:var(--accent)}
.otstatus{font-size:10px;color:var(--muted)}
js
const cells = [...document.querySelectorAll('.otcell')];
const status = document.getElementById('otstatus');
cells.forEach((c, i) => {
  c.addEventListener('input', () => {
    auto = false;
    c.toggleAttribute('data-filled', !!c.value);
    if (c.value && cells[i + 1]) cells[i + 1].focus();
  });
  c.addEventListener('keydown', (e) => {
    if (e.key === 'Backspace' && !c.value && cells[i - 1]) { auto = false; cells[i - 1].focus(); }
  });
});
let auto = true;
function loop() {
  if (!auto) return;
  let i = 0;
  const t = setInterval(() => {
    if (!auto) return clearInterval(t);
    cells[i].value = String(Math.floor(Math.random() * 10));
    cells[i].setAttribute('data-filled', '');
    i++;
    if (i >= cells.length) {
      clearInterval(t);
      status.textContent = '확인 중…';
      setTimeout(() => {
        if (!auto) return;
        status.textContent = '인증 완료';
        setTimeout(() => {
          if (!auto) return;
          cells.forEach((c) => { c.value = ''; c.removeAttribute('data-filled'); });
          status.textContent = '문자로 받은 6자리를 입력하세요';
          setTimeout(loop, 900);
        }, 1200);
      }, 500);
    }
  }, 220);
}
setTimeout(loop, 500);

It looks like several one-digit boxes (usually six), but the implementation recommended for accessibility and autofill is a single <input> with autocomplete="one-time-code" and inputmode="numeric" — that lets iOS/Android surface the SMS code as a keyboard banner, and the WebOTP API can hand the code to your JS directly after one user consent. If you still want the boxed look, splitting one input visually with letter-spacing and keeping real separate inputs per box (auto-advancing focus on each keystroke) are both common — the latter needs a positional label on each box, like "digit 1 of 6."

It differs from a number input (spinner) — a number input is tuned for raising or lowering one quantity, while an OTP input is tuned for a fixed-length string that needs to be filled fast, via paste or autofill, within a short window.

Supporting paste (dropping all six digits at once) needs its own handling in the paste event — slicing the string and distributing one character per box.