Accessibility pattern · Form inputs

One-time code

Six separate inputs break paste, SMS autofill and screen reader reading. This is one input with autocomplete="one-time-code", drawn as six boxes behind it. It checks the code after a short pause once all six digits are in, keeps a Verify button for everyone, and counts down to Resend without announcing every second.

WCAG criteria
7
Keyboard rules
6
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Check your phone

We sent a 6-digit code by SMS to the number ending in 4521.

6 digits. We check it as soon as all 6 are in.

Didn't get it? Available in 0:30

For this demo, the code is 246810

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the code field, then Verify, then Resend code.
0–9Types a digit into the next box; anything that is not a digit is ignored.
BackspaceDeletes the digit before the caret, as in any text field.
Arrow Left or Arrow RightMoves the caret between digits; the highlighted box follows it.
CtrlVPastes a whole code, even with spaces or a dash in it, and fills every box.
EnterChecks the code straight away, from the field or the Verify button.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the fieldVerification code, edit text. 6 digits. We check it as soon as all 6 are in.
A wrong code is checkedThat code isn't right. Check the latest SMS, or ask for a new code.
The right code is checkedPhone number verified
Focus reaches Resend code during the waitResend code, button, unavailable. Available in 0:24
The wait endsYou can ask for a new code now
Resend code is pressedNew code sent

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-otp-input" data-ap-otp-input data-code="246810" data-wait="30">
  <form class="ap-otp-input__form" novalidate>
    <span class="ap-otp-input__badge" aria-hidden="true">
      <svg viewBox="0 0 24 24" focusable="false"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M10.5 18.5h3"/><path d="m9.5 10 2 2 3.5-3.5"/></svg>
    </span>
    <h3 class="ap-otp-input__name">Check your phone</h3>
    <p class="ap-otp-input__lead">We sent a 6-digit code by SMS to the number ending in 4521.</p>

    <div class="ap-otp-input__field">
      <label class="ap-label" for="otp-input-code">Verification code</label>
      <p class="ap-hint" id="otp-input-hint" data-ap-hint>6 digits. We check it as soon as all 6 are in.</p>
      <div class="ap-otp-input__boxes">
        <input class="ap-otp-input__real" id="otp-input-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" pattern="[0-9]{6}" spellcheck="false" aria-describedby="otp-input-hint otp-input-error" />
        <span class="ap-otp-input__cells" aria-hidden="true">
        <span class="ap-otp-input__cell"></span>
        <span class="ap-otp-input__cell"></span>
        <span class="ap-otp-input__cell"></span>
        <span class="ap-otp-input__gap"></span>
        <span class="ap-otp-input__cell"></span>
        <span class="ap-otp-input__cell"></span>
        <span class="ap-otp-input__cell"></span>
        </span>
      </div>
      <p class="ap-error ap-otp-input__error" id="otp-input-error" role="alert"></p>
    </div>

    <button type="submit" class="ap-btn ap-btn--primary ap-otp-input__verify" data-ap-verify>Verify</button>
    <p class="ap-otp-input__done" role="status" data-ap-say></p>

    <p class="ap-otp-input__resend" data-ap-resend-row>
      <span>Didn't get it?</span>
      <button type="button" class="ap-otp-input__link" data-ap-resend aria-disabled="true" aria-describedby="otp-input-wait">Resend code</button>
      <span class="ap-otp-input__wait" id="otp-input-wait" data-ap-wait><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 7v5l3 2"/><circle cx="12" cy="12" r="9"/></svg><span>Available in</span> <span class="ap-otp-input__clock" translate="no" data-ap-left>0:30</span></span>
    </p>
  </form>
  <p class="ap-otp-input__demo"><span>For this demo, the code is</span> <code translate="no">246810</code></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.5 Identify Input Purpose Level AA

    autocomplete="one-time-code" tells browsers and phones what the field is for, so they can offer the code from an SMS.

  • 2.2.1 Timing Adjustable Level A

    The countdown limits only how soon a new code can be asked for; the field itself never times out while someone types.

  • 3.2.2 On Input Level A

    The automatic check is described in the hint before anyone types, waits for a pause, and can be switched off; Verify is always there.

  • 3.3.1 Error Identification Level A

    A wrong or short code is described in words, the field is marked invalid, and the message is tied to the field.

  • 3.3.2 Labels or Instructions Level A

    The label, the expected length and how checking works are shown before the field and read with it.

  • 4.1.2 Name, Role, Value Level A

    One labelled text field holds the whole code, so its name, value and invalid state are exposed as one control.

  • 4.1.3 Status Messages Level AA

    Errors are announced as alerts; success, a sent code and the end of the wait as polite status messages.

Usage

When to use it

Use it

  • Confirming a phone number or an email address with a short code sent to it.
  • Two-step sign-in where the code comes by SMS, email or an authenticator app.

Use something else

  • Long codes with letters, like recovery codes: use a plain text field with the format in its hint.
  • Any code people would have to copy by hand across devices when a link or a passkey would do.

Common failures

How it usually goes wrong

  • Six separate inputs

    Six fields break paste and SMS autofill, read as six unnamed fields, and need focus juggling on every key. One field drawn as six boxes keeps all of that native.

  • Submitting the instant the sixth digit lands

    People who mistype the last digit get an error before they can fix it. The check here waits for a pause and can be turned off.

  • No button to submit

    When the only way to submit is to finish typing, nobody can retry the same code or submit a pasted one that did not trigger it.

  • A countdown that talks every second

    A live timer interrupts whatever is being read, sixty times a minute. This one is silent; it is read on request, and only its end is announced.

  • Resend shown as a disabled link

    A disabled control drops out of the Tab order, so nobody learns why it is unavailable. This button stays focusable with aria-disabled and says how long is left.

  • Blocking paste

    Codes are copied from messages and apps. The paste handler here cleans spaces and dashes out of a pasted code instead of refusing it.

Notes

Building it

  • The real input sits over the six boxes with transparent text; the boxes are aria-hidden drawings of its value, so assistive technology meets one ordinary field.
  • Keep type="text" with inputmode="numeric": type="number" drops leading zeros, adds a spinner and accepts letters like e.
  • maxlength cuts a pasted "246 810" to "246 81" before any script sees it, so the paste handler reads the clipboard and keeps only digits.
  • When forced colors are on, the boxes step aside and the field shows its own text, which the system colors keep readable.
  • End the SMS with the origin-bound line that the WebOTP API reads, and Chrome on Android can offer the code in one tap.

Sources: HTML: autocomplete one-time-code · GOV.UK Design System: Confirm a phone number

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction