GitHub

Components / Form & Input / Input OTP

Input OTP beta

Six boxes for a code that arrives somewhere else — typed, pasted, or autofilled as one.

Preview
<div class="k-otp" data-otp role="group" aria-label="Verification code">
  <input type="text" inputmode="numeric" autocomplete="one-time-code" aria-label="Digit 1" value="4">
  <input type="text" inputmode="numeric" aria-label="Digit 2" value="8">
  <input type="text" inputmode="numeric" aria-label="Digit 3" value="2">
  <span class="gap"></span>
  <input type="text" inputmode="numeric" aria-label="Digit 4">
  <input type="text" inputmode="numeric" aria-label="Digit 5">
  <input type="text" inputmode="numeric" aria-label="Digit 6">
</div>
.k-otp{display:flex;gap:8px;align-items:center;font-family:var(--k-font)}
.k-otp input{width:44px;height:50px;border:1px solid var(--k-line);border-radius:10px;background:var(--k-bg);color:var(--k-ink);font-family:var(--k-mono);font-size:19px;font-weight:600;text-align:center;outline:none;transition:border-color .13s, box-shadow .13s}
.k-otp input:focus{border-color:var(--k-accent);box-shadow:0 0 0 3px var(--k-accent-bg)}
.k-otp .gap{width:10px;height:2px;border-radius:2px;background:var(--k-line)}
const otpBoxes = [...document.querySelectorAll('[data-otp] input')];
otpBoxes.forEach((box, i) => {
  box.addEventListener('input', () => {
    box.value = box.value.replace(/\D/g, '').slice(-1);
    if (box.value && i < otpBoxes.length - 1) otpBoxes[i + 1].focus();
  });
  box.addEventListener('keydown', e => {
    if (e.key === 'Backspace' && !box.value && i > 0) otpBoxes[i - 1].focus();
    if (e.key === 'ArrowLeft' && i > 0) otpBoxes[i - 1].focus();
    if (e.key === 'ArrowRight' && i < otpBoxes.length - 1) otpBoxes[i + 1].focus();
  });
  box.addEventListener('paste', e => {
    e.preventDefault();
    const text = (e.clipboardData.getData('text') || '').replace(/\D/g, '');
    text.split('').slice(0, otpBoxes.length - i).forEach((ch, j) => { otpBoxes[i + j].value = ch; });
    otpBoxes[Math.min(i + text.length, otpBoxes.length - 1)].focus();
  });
});

Usage

For verification codes and short PINs. One component, one inputmode, no per-box tab stops to fight with.

Accessibility

Each box is a real input with inputmode="numeric" and autocomplete="one-time-code" on the first, so password managers and SMS autofill land properly. Arrow keys and paste are handled.

Related parts