Field Notes

Tab order is a promise the DOM makes

The Tab key follows the HTML, not the layout. CSS cannot change that order, a positive tabindex hijacks it, and aria-hidden does not remove a stop. One real walk found 162 stops where a list of the markup said 64. Here is why, and how to check.

Published

  • keyboard
  • focus
  • layout
  • wcag 2.2

Press Tab on a page and focus moves to the next thing. Which thing? Not the next thing on screen. The next focusable element in the HTML, in the order the elements were written. That is the promise. Everything that breaks keyboard navigation breaks it by making the screen and the HTML tell different stories.

The rule, with its one exception

The browser keeps one sequence. It is the DOM order of every focusable element: links with an href, buttons, form fields, and anything with tabindex="0". Elements that are display: none or visibility: hidden are left out. Disabled controls are left out.

The one exception is a positive tabindex. Any element with tabindex="1" or higher goes to the front of the sequence, lowest number first, before every element without one. So a single tabindex="5" on a button in the middle of a page makes it the first stop, ahead of the header. The fix is never a bigger number on something else. The ruleset calls this AW-KEY-002, and the Tab Walk lab has a checkbox that does exactly this to a Buy button so you can watch stop 1 move.

What CSS cannot do

CSS can put a card anywhere. order, flex-direction: row-reverse, grid placement, absolute positioning, floats. None of it changes the Tab sequence. So a row of three cards reordered with order: 3, 1, 2 still Tabs in the written order 1, 2, 3, which on screen is middle, right, left. Focus jumps backwards across the row. A sighted keyboard user sees it happen and is confused. A screen reader user hears the right order and never knows the screen disagrees.

The Reading Order note covers this from the screen reader's side. From the keyboard's side the lesson is the same. If a card should come first, put it first in the HTML. AW-ORDER-001 flags the CSS; AW-ORDER-002, added with this note, asks an agent to walk the page and compare the stops it visits with where they sit on the screen.

Three ways to hide, two of which work

Hiding On screen In the Tab sequence Announced
display: none No No No
visibility: hidden No, space kept No No
aria-hidden="true" Yes Yes No

The third row is the trap. aria-hidden hides an element from the accessibility tree and from nothing else. The link is still on the page and still a Tab stop. A screen reader user presses Tab, lands on it, and hears nothing, because the thing they are on has been declared not to exist. That is AW-ARIA-002, and the lab's hide control shows the stop staying in the sequence with a ghost badge.

Stops that do nothing

tabindex="0" on a container, a card, a list item, or a heading, makes it a Tab stop. Nothing happens when you reach it. Three cards with tabindex="0" are three extra presses between the hero and the form, each landing on a box that does not respond to Enter. The fix is to leave tabindex off anything that is not interactive. A container that needs to receive focus from a script gets tabindex="-1", which lets a script focus it without putting it in the sequence.

Count by walking, not by listing

Here is the number that changed how the lab works. One real run on a large public home page listed 64 focusable elements from the markup and then walked them with Tab. The walk visited 162 stops.

Focusable elements listed from the markup against stops visited by a Tab walk, one real page Two horizontal bars. Listed from the markup: 64. Visited by walking Tab: 162. The walk found two and a half times as many stops. 0170 stops Listed from the markup64 Visited by walking Tab162
Method Stops What it sees
List focusable elements in the markup 64 Elements present when the page is read
Walk the page with Tab 162 Everything focus actually lands on, including controls that appear only when something before them gets focus

Menus open when their button is focused. Disclosures expand. Carousels reveal their dots. A list made from the markup sees the page at rest. A walk sees the page as a keyboard user meets it. When the two numbers differ, the walk is the one that happened.

The lab plants a small version of this: a "More ways to pay" button that opens on focus and adds three links. At rest the sample page has 13 stops. Walked, it has 16.

Checking a page

  • Press Tab from the top and say each stop out loud. If you say something that is behind or to the left of the last thing you said, the order is broken.
  • Search the CSS for order:, row-reverse, column-reverse, and explicit grid-row or grid-column. Each one is a place where the screen may disagree with the HTML.
  • Search the HTML for tabindex= with a value above 0. Remove it and fix the DOM instead.
  • Search for aria-hidden="true" on anything that contains a link, button, or field. Either remove the focusable content or hide it for real.
  • If you can script a browser, walk Tab and count. Compare the count with your list. Trust the walk.

Try it

Open the Tab Walk lab, press the walk button, and watch the numbers. Then tick each checkbox in turn and walk again. The readout under the page says how many stops moved out of reading order and how many are hidden but still reachable.

Sources

  • w3.org, Understanding Success Criterion 2.4.3: Focus Order
  • w3.org, Understanding Success Criterion 2.4.7: Focus Visible
  • html.spec.whatwg.org, the tabindex attribute and sequential focus navigation order

Open the companion lab

  • The sticky header that eats your focus

    Shift+Tab back up a form and the field you land on slides under the sticky header. The browser scrolled just far enough, and just far enough is behind the bar. WCAG 2.2 calls it focus not obscured. One CSS property fixes it.

  • Where focus goes when a dialog opens

    A dialog owes a keyboard user three things. Put focus inside it on open. Keep Tab inside while it is open. Give focus back to the opener on close. Miss any one and the reader is lost behind the dialog or dropped at the top of the page.

  • Twenty-four pixels is a smaller promise than it sounds

    WCAG 2.2 asks for pointer targets of 24 by 24 CSS pixels. The spacing exception means a row of 18 px icons can pass and a row of 24 px icons can be hard to hit. Here is the rule, the circle, and the five exceptions, drawn.

Spotted something this note gets wrong?

Corrections and better examples are the best kind of message.

Send a correction