Search topics
Code boxes where a keyboard can't correct a wrong digit
One-time code boxes move on to the next box when a key comes up, so Shift+Tab is sent straight back and the keyboard can't return to a mistyped digit.
box.addEventListener('keyup', …) The problem
Keyboard, switch and screen reader users who mistype one digit can't go back to it, so they can't send the right code.
A shop asks for a second step when people sign in. It sends a six-digit code by text message and shows six boxes, one per digit. Each box moves on to the next as soon as it is full, so the code can be typed in one go without pressing Tab. The message says "Your Larkspur code is 481902". Someone types 4, then 8, then 7 by mistake, and focus is already in the fourth box.
They press Backspace. Nothing happens: the fourth box is empty, so there is nothing to delete. They press Shift+Tab to go back: focus reaches the third box and jumps straight on to the fourth again. The left arrow key does nothing either. With a mouse, the slip takes a second: click the 7, delete it, type 1. From the keyboard, no key reaches the 7. What is left is to send a code they know is wrong, or to leave the page and start the sign-in again.
Success criterion 2.1.1 Keyboard, level A, asks that everything a page lets people do can be done from a keyboard. Correcting a digit is something this page lets a mouse do, by clicking the box, so the keyboard must be able to do it too.
The script causes it by listening for keyup. A key acts when it goes down, and a keyup handler runs when it comes up, on whatever has focus by then. Shift+Tab moves focus to the third box as the key goes down. When Tab comes up, its keyup arrives at the third box, which is full, so the script sends focus on to the fourth. Every key does the same, so no key can stay on a box that holds a digit. Backspace and the arrow keys act inside the box that has focus, which is empty, and nothing in the script takes them back. Tab forward still leaves the boxes, so this is not a keyboard trap: it is the way back that is closed. We saw the same in Chromium, Firefox and Safari's engine.
Boxes that move on at the input event instead, as in one-time code boxes that won't take a pasted code, don't fail this way, even with no Backspace handling. The input event fires only when the value changes, so Shift+Tab stays on the third box, the browser selects its digit as Tab arrives, and typing 1 replaces the 7. The topic's other forms fail for other reasons: boxes drawn as div elements and filled by a key handler on the page have none of a field's keys unless the script adds each one.
Sign-in codes often run out within minutes, and a few wrong codes can lock the account, so a digit that can't be corrected costs more than a retype.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Limited manipulation Keyboard and switch users can type the code, but not undo a slip in it: Shift+Tab bounces straight back to the empty box, and Backspace and the arrow keys stay where they are. People with hand tremors, who often use a keyboard because a mouse is hard to use, meet it too.
- Without vision Screen reader users move between the boxes with Tab and Shift+Tab. When they hear that the third box holds 7, Shift+Tab can't keep them there: focus moves on as soon as it arrives.
Every box has a label, so axe and other scanners pass the page; nothing in the markup says when the script moves focus. EaseWeb's audit types two digits into the first boxes of a row of four or more maxlength="1" fields and presses Shift+Tab, and reports ew:code-boxes-keyboard when focus is sent straight back to the third box. It types only two digits, never a whole code, which might send itself. It doesn't check boxes that aren't input elements. Moving on at keyup and comparing the value's length with maxlength is a way auto-advance has long been written in forum answers and plugins, and it works for anyone who types the code right first time, so a test that types it right finds nothing.
Try it
Type 4, 8 and 7, then change the 7 to a 1 using only the keyboard.
Try Backspace, the left arrow and Shift+Tab. The panel shows where each key sent focus, including any box it passed through on the way.
Broken Can you fix this?
Last key: none yet
The fix
Move on only when a digit goes in, or take the code in one field.
- 1Move on to the next box only when a digit goes in, never on every key.
- 2Let the keyboard reach any digit already typed, and type over it.
- 3Where the design allows, ask for the code in one field.
<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" maxlength="1" aria-label="Digit 1 of 6"> <input name="code-2" inputmode="numeric" maxlength="1" aria-label="Digit 2 of 6"> <input name="code-3" inputmode="numeric" maxlength="1" aria-label="Digit 3 of 6"> <input name="code-4" inputmode="numeric" maxlength="1" aria-label="Digit 4 of 6"> <input name="code-5" inputmode="numeric" maxlength="1" aria-label="Digit 5 of 6"> <input name="code-6" inputmode="numeric" maxlength="1" aria-label="Digit 6 of 6"></div><button type="submit">Continue</button>One field for the whole code
<label for="code">Security code</label><p id="code-hint">Enter the 6-digit code we sent to your phone.</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>Why this fix: Use native HTML first
The handler runs for every key that comes up, not only for a typed digit. When Shift+Tab reaches a full box, the Tab key's keyup arrives there and moves focus on again, so the keyboard can never stay on a digit to change it.
In one text field, every editing key works as it does in any field: Backspace deletes the 7, the arrow keys move the cursor, and Shift+Tab goes back to whatever came before. Nothing moves focus, so nothing can move it the wrong way. The field also takes a pasted code whole, and autocomplete="one-time-code" lets a browser that supports it offer the code from a new message.
All three let the keyboard correct a digit. Use the one field: it does the job with no script, every editing key and paste work because the browser handles them, and there is nothing to keep working. Changing keyup to input is the quickest repair for boxes already live, but it leaves the paste failure and a Backspace that doesn't go back, so treat it as a first step. Keep boxes only when the design can't change, and then give them all the keys, as in the last option, and say beside them that each box moves on to the next (see Implementation decisions).
Move focus on from the input event, which fires only when the value changes, and never from a keyup or keydown handler that acts on every key, Tab, Shift and the arrow keys included. A check such as "is the box full?" doesn't help, since a box that is full and has just been reached by Shift+Tab is exactly the case that goes wrong.
Browsers select a field's contents when Tab or Shift+Tab reaches it, which is why typing over a digit works in the smaller fix. A click leaves the cursor instead, as does a script's focus() in Chromium and Firefox; with maxlength="1", a digit typed beside another is then refused. The last option selects the digit on focus, and its input handler keeps only the digit just typed, so typing replaces the digit however the box was reached, even in Safari's engine, where a click clears the selection again.
The two box fixes still move focus by themselves, and moving focus is a change of context under 3.2.2 On Input. The Understanding document's own example of split phone fields that move on by themselves passes because the form describes that behaviour at the start, so write it into the instruction above the boxes, such as "Each box moves on to the next as you type." One field moves nothing, which is one more reason to prefer it.
Let people send the code themselves, with the button or Enter. A form that sends the code the moment the last box fills changes the page without being asked, as dropdowns and fields that change the page on their own describes, and a mistyped digit becomes a failed attempt before anyone could correct it.
A Japanese input method can type digits at full width (0 to 9). \D removes those, so a box that keeps only 0 to 9 can end up empty with no reason given; convert them first.
Verify the fix
4 checks.
Test with: Keyboard, Screen reader, Mouse
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 code entry. The example is a sign-in code; a PIN, a gift card number or a booking reference asked for in boxes needs the same test. Pasting the whole code is covered in one-time code boxes that won't take a pasted code. Boxes built from elements that aren't inputs need every key added by script, and a screen reader may not treat them as fields at all; this guide doesn't cover them in code. Test the whole sign-in with the keyboards, switches and screen readers your users rely on.
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.
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.