# One-time code boxes that won't take a pasted code

Source: https://easeweb.dev/learn/one-time-codes
Topics: Forms > One-time codes can be pasted into one field (WCAG 3.3.8, 2.1.1)
The fix: Let the whole code go in at once. Either ask for it in one text field with autocomplete="one-time-code" and no paste blocking, or keep the boxes but drop maxlength="1" and spread a pasted, autofilled or dictated code across them with a script.
Test with: keyboard, screen reader, touch, autofill, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F109 https://www.w3.org/WAI/WCAG22/Techniques/failures/F36 https://design-system.service.gov.uk/patterns/confirm-a-phone-number/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop asks for a second step when people sign in. After the password, it sends a six-digit code by text message and shows a page with six boxes, one per digit. The message arrives: "Your Larkspur code is 481902". The person copies the code and pastes it into the first box. Only the 4 goes in. Each box takes one character, so the browser cuts the pasted code down to its first digit, and the other five boxes stay empty. The code the phone offers from the new message may go in the same way. What is left is to read the code in the message and type it back, digit by digit, from one screen into another.

The same step fails in other ways:

- The boxes take a paste, but only of exactly six digits, so a code copied with a space in the middle, as some messages write it, does nothing.
- One code field has `maxlength="6"`, so a code copied as "481 902" is cut to "481 90" and rejected.
- A PIN asked for in four boxes, one digit each, that a password manager can't fill.

## Why it fails

Success criterion 3.3.8, Accessible Authentication (Minimum), is level AA and new in WCAG 2.2. No step of signing in may rely on a cognitive function test, such as remembering a password or transcribing a code, unless there is another way to sign in or something that helps, such as a password manager or copy and paste. Typing a code from a message is transcription. The Understanding document says a service that needs a verification code typed by hand fails, and that people must at least be able to paste it.

Failure F109 covers the boxes: a code entered one character per field fails, unless the whole code can be pasted into the first field and the other fields fill in by themselves. A field that blocks paste fails the same way, because it leaves typing as the only way in. Either still passes if the same sign-in offers another way that needs no transcription, such as an emailed sign-in link.

The criterion covers getting the code into the page, not getting it to the person. Whether the code can reach the device people sign in on, or be copied there from another device, is outside its scope.

## Who is affected

On a phone the message and the page are in different apps, so typing the code means switching between them, often more than once, with a timer running on the page.

## What automation and AI miss

Each box has a label, so axe and other scanners pass the page. EaseWeb's audit reports a row of one-character boxes that nothing on the page listens to a paste for, since the browser cuts any paste there to one character, and a code field that cancels a test paste without putting anything in. When the boxes have their own paste handling and a test paste doesn't spread across them, it sends a lead instead: it pastes only two digits, never a whole code, because a full code may send itself and count as a failed attempt on the real account, so a person has to try a real one. It can't tell whether the code arrives, or whether a phone offers it. A code assistant asked for an "OTP input" commonly produces six boxes with `maxlength="1"` that jump to the next box as each digit is typed, and no paste handling at all.

## Before: six boxes that take one character each

```html
<p id="code-label">Enter the 6-digit code we sent to your phone</p>
<div class="code-boxes" role="group" aria-labelledby="code-label">
  <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">
  <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>
```

```js
const boxes = [...document.querySelectorAll('.code-boxes input')];
// Typing a digit moves on to the next box.
boxes.forEach((box, i) => box.addEventListener('input', () => {
  if (box.value && boxes[i + 1]) boxes[i + 1].focus();
}));
```

`maxlength="1"` makes the browser cut a pasted code, an autofilled one or a dictated one down to its first character before the script's `input` handler sees it. Nothing spreads the rest across the other boxes, so the code can only be typed in, one digit at a time.

## After: one field that takes the whole code

```html
<label for="code">Security code</label>
<p id="code-hint">We sent a 6-digit code to your phone. It works for 15 minutes.</p>
<input id="code" name="code" type="text" inputmode="numeric"
  autocomplete="one-time-code" spellcheck="false" aria-describedby="code-hint">
<button type="submit">Continue</button>
```

The code goes in the way any text does: pasted with the keyboard or the context menu, dictated, or filled in by the browser. `autocomplete="one-time-code"` tells the browser what the field is for, so a browser that supports it, such as Safari, can offer the code from a new message above the keyboard. `inputmode="numeric"` brings up the number keypad on a phone. There is no `maxlength`, so a code pasted as "481 902" still goes in; the server takes out spaces and hyphens before it checks the code. No script is needed for any of this.

## After: boxes that spread a pasted code across them

```html
<p id="code-label">Enter the 6-digit code we sent to your phone</p>
<div class="code-boxes" role="group" aria-labelledby="code-label">
  <input name="code-1" inputmode="numeric" autocomplete="one-time-code" aria-label="Digit 1 of 6">
  <input name="code-2" inputmode="numeric" aria-label="Digit 2 of 6">
  <input name="code-3" inputmode="numeric" aria-label="Digit 3 of 6">
  <input name="code-4" inputmode="numeric" aria-label="Digit 4 of 6">
  <input name="code-5" inputmode="numeric" aria-label="Digit 5 of 6">
  <input name="code-6" inputmode="numeric" aria-label="Digit 6 of 6">
</div>
<button type="submit">Continue</button>
```

```js
const boxes = [...document.querySelectorAll('.code-boxes input')];
boxes.forEach((box, i) => {
  // A pasted, autofilled or dictated code arrives as several characters at once:
  // spread its digits over this box and the ones after it.
  box.addEventListener('input', () => {
    const digits = box.value.replace(/\D/g, '').slice(0, boxes.length - i);
    box.value = '';
    [...digits].forEach((digit, n) => { boxes[i + n].value = digit; });
    if (digits) boxes[Math.min(i + digits.length, boxes.length - 1)].focus();
  });
  // Typing over a filled box replaces its digit.
  box.addEventListener('focus', () => box.select());
  // Backspace in an empty box goes back to the one before it.
  box.addEventListener('keydown', (event) => {
    if (event.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus();
  });
});
```

The boxes have no `maxlength`, so a pasted code reaches the script whole, and the script puts one digit in each box, from the box it went into onwards. Because it works on the `input` event, the same code handles a paste, the code a phone offers and dictation. This is the exception F109 describes: the whole code can be pasted into the first box, and the others fill in. It holds only while the script runs, and while nothing else on the page, such as a component library's own paste handler, cuts the code first. The server joins the six values, with any spaces left out, before it checks the code.

## Which option to use

Both pass 3.3.8. Use the one field: it does the same job with no script, and paste, the code a phone offers, dictation and password managers all work there because the browser handles them, where the boxes need a script that handles each of them correctly. A screen reader also reads one field once, not six. Keep the boxes only when the design can't change, and then spread every multi-character input across them as above.

## Implementation decisions

Don't use `type="number"` for a code. A code is not an amount: browsers ignore `maxlength` there, the arrow keys step the value up and down, and a pasted code with a hyphen in it is thrown away as not a number. Use a text field with `inputmode="numeric"`, and for a code that can contain letters, leave `inputmode` out.

Let people send the code themselves, with a button or Enter. A form that sends itself the moment the last digit is in fails 3.2.2 (failure F36) unless the page says beforehand that it will, and a wrong digit then becomes a failed attempt before anyone could check it.

Give the code time. A code that runs out after 30 seconds is a time limit. The Understanding document for 2.2.1 says a time-limited sign-in code set for security can count as essential and be exempt from that criterion, but people who type slowly or switch apps with a screen reader need much longer; GOV.UK gives 15 minutes. A "Send a new code" button, a real button that works with the keyboard, helps people whose code ran out or never came.

Where the design wants the code to look spaced out, style the one field instead of splitting it: `letter-spacing` and `font-variant-numeric: tabular-nums` spread the digits without six boxes.

The same applies to anything else asked for in one-character boxes, such as a PIN. A password manager can fill one field but not six, and someone who can't remember a PIN needs it to be pasted or filled in. Offering another way to sign in that needs no transcription, such as an emailed link or a passkey, also passes 3.3.8, but the code step itself should still take a paste, for the people who choose it.

## Verify the fix

1. Copy a real code from a text message or an email and paste it, with Ctrl + V or Command + V and with the context menu, into the field or the first box, and confirm every digit goes in.
2. On a phone, accept the code the keyboard or the browser offers from a new message, and with voice input dictate a code, and confirm the whole code arrives.
3. Paste a code with a space or a hyphen in the middle, such as "481 902", and confirm it goes in and is accepted.
4. Change one digit with the keyboard: in the boxes, move back to it with Shift + Tab and type over it. Confirm nothing is sent until you press the button.
5. With a screen reader, confirm the field or the group of boxes is read with its label and instruction, and that you can read the code back before sending it.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the sign-in flow or its code component changes.

## Limits

This is a starting pattern, not a guarantee for every sign-in. The example is a code sent by text message; codes from an authenticator app or an email need the same paste test. The criterion doesn't ask a site to send codes by any particular route, and whether the code reaches the device people sign in on is outside it. The rest of signing in, such as a password field that refuses paste or a puzzle to solve, falls under the same criterion. Test the whole sign-in with the people and tools your users rely on.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
