OTP Input

Enter the 6-digit code

We sent it to j•••@example.com.

Didn’t get it?

@use 'sprucecss/scss/spruce' as *;

.otp-input {
  @include layout-stack('m');
  text-align: center;

  &__legend {
    font-size: font-size('h4');
    margin-inline: auto;
  }

  &__hint,
  &__resend {
    color: color('muted');
  }

  &__digits {
    display: flex;
    gap: spacer('xs');
    justify-content: center;
  }

  &__digit {
    aspect-ratio: 1;
    font-family: config('font-family-mono', $typography);
    font-size: font-size('h3');
    font-weight: 700;
    inline-size: 3.25rem;
    padding: 0;
    text-align: center;
  }

  &__resend {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
  }
}
<form class="otp-input" onsubmit="return false;">
    <fieldset>
        <legend class="otp-input__legend">Enter the 6-digit code</legend>
        <p class="otp-input__hint" id="otp-hint">We sent it to j•••@example.com.</p>
        <div class="otp-input__digits" data-otp aria-describedby="otp-hint">
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="one-time-code" aria-label="Digit 1 of 6" required>
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="off" aria-label="Digit 2 of 6" required>
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="off" aria-label="Digit 3 of 6" required>
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="off" aria-label="Digit 4 of 6" required>
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="off" aria-label="Digit 5 of 6" required>
        <input class="form-control otp-input__digit" type="text" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="off" aria-label="Digit 6 of 6" required>
        </div>
    </fieldset>
    <button class="btn btn--primary btn--block" type="submit">Verify</button>
    <p class="otp-input__resend">Didn’t get it? <button class="btn btn--link btn--sm" type="button">Resend code</button></p>
</form>
(() => {
  document.querySelectorAll('[data-otp]').forEach((group) => {
    const inputs = [...group.querySelectorAll('input')];

    const fill = (value, start = 0) => {
      [...value.replace(/\D/g, '')].slice(0, inputs.length - start).forEach((digit, index) => {
        inputs[start + index].value = digit;
      });

      const next = inputs.find((input) => !input.value) ?? inputs[inputs.length - 1];
      next.focus();
    };

    inputs.forEach((input, index) => {
      input.addEventListener('input', () => {
        if (input.value.length > 1) {
          fill(input.value, index);
          return;
        }

        input.value = input.value.replace(/\D/g, '');

        if (input.value && inputs[index + 1]) {
          inputs[index + 1].focus();
        }
      });

      input.addEventListener('keydown', (event) => {
        if (event.key === 'Backspace' && !input.value && inputs[index - 1]) {
          inputs[index - 1].focus();
        } else if (event.key === 'ArrowLeft' && inputs[index - 1]) {
          inputs[index - 1].focus();
        } else if (event.key === 'ArrowRight' && inputs[index + 1]) {
          inputs[index + 1].focus();
        }
      });

      input.addEventListener('paste', (event) => {
        event.preventDefault();
        fill(event.clipboardData.getData('text'), index);
      });

      input.addEventListener('focus', () => input.select());
    });
  });
})();

OTP Input

On this page

A one-time code input split into six boxes, with auto-advance, backspace navigation and paste support.

Technical Details

  • The boxes are grouped in a fieldset with a legend, and each has an "Digit N of 6" label.
  • The first box has autocomplete="one-time-code", so mobile keyboards can offer the code from an SMS; pasting a full code fills every box.
  • Typing moves the focus forward, Backspace on an empty box moves it back, and the arrow keys move between boxes.
  • inputmode="numeric" shows the number keyboard on touch devices.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.