Seven numbered blocks left to right, joined by arrows: arrive, skip, traverse, group, open, work, close. One ordered keyboard walk rather than a catalogue of separate patterns.

Keyboard navigation and focus management: one ordered walk through your page

Keyboard navigation and focus management is not a catalogue of patterns to memorise. Instead it is one ordered walk through a page. You can run it, observe it and write the result down. Below is that walk, in seven stops, with a pass condition at each one.

What an automated accessibility scan can and cannot establish#

An automated scan reports the failures it is able to detect. Also, its publisher says plainly that those are not all of them.

WebAIM is the accessibility research centre at Utah State University. WebAIM publishes an annual report called the WebAIM Million. In the 2026 edition, an automated WAVE scan covered the top 1,000,000 home pages in the Tranco ranking. That scan reported detected WCAG 2 failures on 95.9% of them. In the same scan the average was 56.1 errors per page. Those figures describe what one automated scan found on a million home pages. They do not describe what a million organisations do.

Keyboard behaviour is something a person performs. It is not something markup declares, so a clean report is silence rather than evidence. Consequently, somebody has to close the gap by hand, by pressing keys.

If you have not placed keyboard operability inside the wider picture, read the first seven fixes ranked under POUR alongside this.

Replace the pattern catalogue with one walk: keyboard navigation and focus management as a procedure#

Keyboard navigation and focus management reduces to seven things a keyboard user actually does. First, arrive. Second, skip the repeated blocks. Third, traverse the content. Fourth, enter a grouped widget. Then open a dialog, work inside it, and close it.

Each stop below has three parts. There is an action you perform. There is an observable pass condition. Finally, there is a result worth recording.

The order itself is deliberate. Each stop assumes the one before it works. Still, stops five, six and seven stand on their own. If your actual problem is a dialog, jump straight to them. You do not need the first four stops to use the last three.

Finally, set aside an unhurried half hour for a first run. That is an estimate of your own effort and nothing more. We have not measured it.

Stop one, arrive: where does focus start, and can you see it#

Press Tab once on a freshly loaded page. Focus must move somewhere you can see, and then you record where it landed. That single observation is the first result of the walk.

Two success criteria sit under this stop. WCAG 2.2 Success Criterion 2.1.1 Keyboard, at Level A, requires this: "All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints."

Beside it sits WCAG 2.2 Success Criterion 2.4.7 Focus Visible, at Level AA. It asks that "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible."

Run this in whichever browser you already have open. Styling the indicator is a separate question with a real answer elsewhere. For that, see the spec-accurate :focus-visible rules and the 2026 outline pattern. In short, treat styling here as one checklist item.

A skip link is the only escape from a repeated navigation block. In the 2026 WebAIM Million, an automated scan of the top 1,000,000 home pages found a skip link present on 17.1% of them. In the same scan, one out of every ten skip links was broken. Either they were hidden in a way that made them inaccessible, or the link target was not present in the page.

Those counts describe what that scan detected on home pages. WebAIM's own limitation applies here too. Not all conformance failures can be automatically detected.

WCAG 2.2 Success Criterion 2.4.1 Bypass Blocks, at Level A, requires that "A mechanism is available to bypass blocks of content that are repeated on multiple web pages."

The test has two halves. First, press Tab once and watch for the link to appear. Second, activate it and confirm that focus lands inside the target. A link that appears but moves nothing passes a visual check. It still fails a keyboard user completely.

Stop three, traverse: tab order follows the markup, not the layout#

The document, not the screen you are looking at, orders sequential focus navigation. The HTML Living Standard says this of elements whose tabindex value is zero: "The relative ordering within a tabindex-ordered focus navigation scope for elements and focusable areas that belong to the same focus navigation scope and whose tabindex value is zero should be in shadow-including tree order."

That sentence covers elements carrying an explicit tabindex="0". The three bare buttons below carry no tabindex attribute at all, and the specification orders that case in shadow-including tree order as well, so the same prediction holds for them.

In short, shadow-including tree order is markup order, with each shadow tree flattened at its host's position. Consequently, any visual arrangement that disagrees with the markup produces a tab sequence that disagrees with the page. This is a specified behaviour rather than a style preference. Therefore you can predict a tab sequence from the markup before you ever test it.

The prediction and the screen may disagree. In that case the criterion at stake is WCAG 2.2 Success Criterion 2.4.3 Focus Order, at Level A: "If a web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability."

The CSS that silently decouples visual order from tab order#

Next, consider four common CSS mechanisms that move a control on screen. However, none of them moves it in the markup. Take one block of markup and apply each in turn.

four mechanisms · css
/* 1. flex order */
.row { display: flex; }
#c   { order: -1; }

/* 2. reversed direction */
.row { display: flex; flex-direction: row-reverse; }

/* 3. explicit grid line placement */
.row { display: grid; grid-template-columns: repeat(3, 1fr); }
#a   { grid-column: 3; }

/* 4. absolute positioning */
.row { position: relative; }
#b   { position: absolute; right: 0; }
One markup block, four CSS mechanisms, one tab sequence

1. flex order#

.tod-row { display: flex; }
.tod-item:nth-child(3) { order: -1; }
Screen order (specified ordering)C -> A -> B
Tab sequence (specified ordering)A -> B -> C

The screen shows C, A, B. The tab sequence stays A, B, C.

2. reversed direction#

.tod-row { display: flex; flex-direction: row-reverse; }
Screen order (specified ordering)C -> B -> A
Tab sequence (specified ordering)A -> B -> C

The screen shows C, B, A. The tab sequence stays A, B, C.

3. explicit grid line placement#

.tod-row { display: grid; grid-template-columns: repeat(3, 1fr); }
.tod-item:nth-child(1) { grid-column: 3; }
Screen order (specified ordering)B -> C -> A
Tab sequence (specified ordering)A -> B -> C

A has moved to the far right. The tab sequence stays A, B, C.

4. absolute positioning#

.tod-row { position: relative; }
.tod-item:nth-child(2) { position: absolute; right: 0; }
Screen order (specified ordering)A -> C -> B
Tab sequence (specified ordering)A -> B -> C

B is taken out of flow and sits at the far right. The tab sequence stays A, B, C.

<div class="row">
  <button id="a">A</button>
  <button id="b">B</button>
  <button id="c">C</button>
</div>
Every mechanism, its screen order, and the tab sequence beside it.
MechanismScreen orderTab sequenceWhat it shows
1. flex orderC -> A -> BA -> B -> CThe screen shows C, A, B. The tab sequence stays A, B, C.
2. reversed directionC -> B -> AA -> B -> CThe screen shows C, B, A. The tab sequence stays A, B, C.
3. explicit grid line placementB -> C -> AA -> B -> CA has moved to the far right. The tab sequence stays A, B, C.
4. absolute positioningA -> C -> BA -> B -> CB is taken out of flow and sits at the far right. The tab sequence stays A, B, C.

Each mechanism's CSS is applied live to three real buttons, so the screen order is what your own browser does with that rule. The tab sequence beside it is derived from the specified ordering rule rather than from a browser run. The one measurement in this figure is your own Tab press.

These are the sequences the specified ordering yields for this markup. No browser run produced them.

Now apply the ordering rule quoted above. In every one of these arrangements the tab sequence stays A, B, C. Because none of them changes the markup, the sequence cannot move. Meanwhile the screen shows C, A, B in the first. In the second it shows C, B, A. In the third, A has moved to the far right.

These are the sequences the specified ordering yields for this markup. No browser run produced them.

That is why the markup looked fine in review. The defect lives in the gap between two files, and neither file is wrong on its own. This claim is scoped to these four mechanisms. Plenty of other CSS leaves both order and layout alone.

tabindex is a three-way decision, not three values to memorise#

A focus navigation scope is the set of elements the browser walks when you press Tab. The tab sequence is the order it walks them in. The tabindex attribute changes an element's place in that walk. Also, the HTML Living Standard defines each of the three cases.

For a negative value, the specification says this: "The user agent must consider the element as a focusable area, but should omit the element from any tabindex-ordered focus navigation scope." In short, tabindex="-1" keeps an element focusable by script and by click, while removing that element from the tab sequence. Consequently, it is the enabling half of every grouped-widget pattern below.

For zero, the specification says the user agent must allow the element to be considered as a focusable area. Ordering then follows shadow-including tree order, as quoted at stop three.

In contrast, a positive value places the element ahead of others. Specifically, the specification orders it before any focusable area whose element has tabindex omitted, and before any whose value is less than or equal to zero. As a result, one positive value reorders an entire scope around itself. The ARIA Authoring Practices Guide puts it more bluntly, noting that "values greater than 0 are strongly discouraged."

The APG is a W3C Working Group Note rather than a W3C Recommendation. That status holds for that quotation and for every APG quotation after it. Consequently, its guidance is expert authoring advice. It does not carry the conformance weight of the WCAG success criteria named at the stops.

Elements that are already focusable#

Several elements are focusable already, so adding tabindex="0" to them is redundant. The specification's list includes:

  • a elements that have an href attribute
  • button elements
  • input elements whose type attribute is not in the Hidden state
  • select elements
  • textarea elements
  • summary elements that are the first summary element child of a details element
  • elements with a draggable attribute set

Stop four, enter a grouped widget: one tab stop, arrows inside#

A set of related controls should cost one press of Tab to reach. Then arrow keys should move among its members. The ARIA Authoring Practices Guide states that "the tab sequence should include only one focusable element of a composite UI component. Once a composite contains focus, keys other than Tab and Shift + Tab enable the user to move focus among its focusable elements."

No WCAG success criterion covers stop four on its own. None was established for it, and mapping one in to fill a column would be inventing an obligation. 2.1.1 Keyboard, at Level A, still reaches every control inside the composite.

In practice, the pass condition is a count. Tab from the control before the widget to the control after it. Then count the presses, where one press is the target. Six presses across a six-button toolbar is the finding, and the number is what you record.

Also, the ARIA role a widget carries decides its expected keys. That half is covered in what a role like menu, dialog or tablist obliges you to do with the keyboard.

Roving tabindex is three operations, in this order#

Roving tabindex gives a group of controls one tab stop, and arrow keys move focus inside it. The APG describes the setup: "When using roving tabindex to manage focus in a composite UI component, the element that is to be included in the tab sequence has tabindex="0" and all other focusable elements contained in the composite have tabindex="-1"."

On each arrow key, the APG specifies three operations: "set tabindex="-1" on the element that has tabindex="0". Set tabindex="0" on the element that will become focused as a result of the key event. Set focus, element.focus(), on the element that has tabindex="0"."

For example, take a toolbar of formatting buttons.

moveTo · javascript
function moveTo(next) {
  const current = toolbar.querySelector('[tabindex="0"]');
  current.setAttribute('tabindex', '-1'); // 1. clear the old
  next.setAttribute('tabindex', '0');     // 2. set the new
  next.focus();                           // 3. then focus
}

The order is the diagnosis. Clear without setting, and the toolbar has zero tab stops, so Tab skips it entirely. Set without clearing, and it has two, so Tab lands inside it twice. Call focus() first, and the attribute state trails the focus by one key press.

Two more details are worth carrying. First, the APG says to check the re-entry target when the composite loses focus. The element you want focused next should be the one holding tabindex="0". Second, the APG gives its reason for preferring this pattern: "One benefit of using roving tabindex rather than aria-activedescendant to manage focus is that the user agent will scroll the newly focused element into view."

Stop five, open a dialog: the platform picks, and may pick wrong#

Opening a dialog must move focus into it. However, which element receives that focus is a judgement the HTML Living Standard declines to make for you. In its own words: "The dialog focusing steps attempt to pick a good candidate for initial focus when a dialog is shown, but might not be a substitute for authors carefully thinking through the correct choice to match user expectations for a specific dialog."

Your lever is autofocus. The specification says authors should use it on the descendant the user is expected to interact with immediately. Where no such descendant exists, use it on the dialog element itself.

Also, the APG names two cases where the first focusable element is the wrong choice. First, content may carry semantic structure that a user must perceive. Then it advises adding tabindex="-1" to a static element at the start of the content and focusing that. Second, a dialog may carry "the final step in a process that is not easily reversible, such as deleting data or completing a financial transaction". In that case "it may be advisable to set focus on the least destructive action, especially if undoing the action is difficult or impossible".

No WCAG success criterion covers this stop on its own, because none was established for it. 2.1.1 Keyboard, at Level A, still reaches every control inside the dialog.

Stop six, work inside: what the specification owes, and what you still owe#

A modal dialog must contain its tab sequence, and it must render everything behind it inert. The APG states that "Like non-modal dialogs, modal dialogs contain their tab sequence. That is, Tab and Shift + Tab do not move focus outside the dialog." In addition it states that "Windows under a modal dialog are inert."

The HTML Living Standard assigns most of that to the modal entry point. Containment, inertness of the rest of the document and close-request handling on Escape all follow from showModal(). Its steps are "to show a modal dialog given this and null". Meanwhile a dialog opened with show() is non-modal and gets none of it.

The obligation the specification leaves with you#

One obligation stays with you, and the specification is explicit about it.

Inert removes interaction and assistive-technology perceivability, and it dims nothing at all. Also, the same section bounds where inert belongs: "In most cases, authors should not specify the inert attribute on individual form controls. In these instances, the disabled attribute is probably more appropriate."

The APG adds one more item for this stop. It recommends that the tab sequence of all dialogs include a visible element with role button that closes the dialog.

Containment done by hand is where this stop usually fails. WCAG 2.2 Success Criterion 2.1.2 No Keyboard Trap, at Level A, is the criterion it breaks: "If keyboard focus can be moved to a component of the page using a keyboard interface, then focus can be moved away from that component using only a keyboard interface, and, if it requires more than unmodified arrow or tab keys or other standard exit methods, the user is advised of the method for moving focus away."

Stop seven, close and land: restore to the invoker, with a ladder#

On close, focus returns to the element that opened the dialog. The APG states that "When a dialog closes, focus returns to the element that invoked the dialog", and it names exceptions to that. The first, in its words, is that "The invoking element no longer exists. Then, focus is set on another element that provides logical work flow." The APG names a second exception beside it, which this walk does not cover. Also, a native dialog does the bookkeeping itself. The show() steps include "Set this's previously focused element to the focused element."

However, the hard case is a trigger the dialog's own action destroyed. For instance, deleting a row removes the button that opened the confirmation. The APG names the destination for that case as an element that provides logical work flow, and does not say which element that is. The ladder below orders the candidates within that direction, as a recommendation made here rather than as specified or normative guidance. It runs in order:

  1. The nearest surviving sibling in the same list, which keeps a deleted row's neighbour under the hand.
  2. The containing list or region, which catches a collapsed menu whose items are all gone.
  3. That region's heading, given tabindex="-1", which catches a route change that replaced the view.
  4. Never the document body, because focus there restarts the walk from stop one.

Press Escape, then press Tab once and record where focus went. Landing on the body is the failure this ladder exists to prevent. No WCAG success criterion covers this stop on its own, because none was established for it. 2.1.1 Keyboard, at Level A, still reaches every control you land among.

Which of the seven stops automated detection reports#

Automated detection reports the static markup failures in this walk. A missing skip link is visible in markup, and so is a positive tabindex or a missing accessible name.

In contrast, three findings here only exist once somebody presses a key. A containment that leaks is one, a restoration that drops to the body is the second, and a tab order decoupled by CSS is the third.

That split is not a claim that behaviour can never be detected. The negative half is the scanning publisher's own: not all conformance failures can be automatically detected, and absence of detected errors does not indicate that a page is accessible or conformant. The positive half is an observation about this walk, and the publisher warrants nothing about it.

In practice that gives you a sentence for whoever asks why a manual pass is still funded. Your pipeline covers the markup half, while the walk covers the behavioural half. For the fuller argument about layered manual testing, see why an automated scanner cannot tell you whether Tab actually reaches this control. Does that behavioural half need auditing by someone other than you? Then an audit that actually presses Tab through the interface is the shape of work it takes. Which conformance level binds an organisation is a separate, jurisdictional question, and this walk does not answer it.

When not to run this: the keyboard code to delete instead#

You should delete some of what this walk finds rather than make it operable.

A hand-rolled focus trap is the first candidate. showModal() supplies containment, inertness and the recorded previously focused element. Consequently, none of that costs you any code. In contrast, a hand-written trap is code you own forever for no gain. Still, it is warranted in two honest cases. First, a surrounding framework may control rendering so tightly that showModal() is unavailable. Second, a non-modal surface may need containment the specification does not give it.

The second candidate is roving tabindex over three items, which is rarely worth its cost. Three buttons that are three tab stops cost a keyboard user two extra presses. Weigh that against a component with state to keep correct on every key press.

The third case is the widest of them. A grouped widget may exist only because somebody styled a native control away. For example, consider a select styled beyond recognition, or div elements imitating radio buttons. Those need arrow keys, roles and a roving tabindex for one reason only. Somebody replaced the element that had all of it for free. Therefore deleting the widget removes the accessibility work rather than completing it.

Record the walk, and know when to hand it to someone else#

Finally, the walk is worth running only if you write the outcome down. Record a pass or fail per stop, and also record one line of what you observed.

Seven recorded results are not a conformance statement. Running this walk does not establish WCAG conformance for the page, and the table below records behaviour rather than conformance.

The seven stops, what you record at each, and the WCAG 2.2 criterion where one applies
StopWhat you recordCriterion
1. ArriveWhere first Tab landed, and whether it was visible2.1.1 Keyboard (Level A), 2.4.7 Focus Visible (Level AA)
2. SkipLink appeared, and activation moved focus into the target2.4.1 Bypass Blocks (Level A)
3. TraverseSequence preserved meaning and operability2.4.3 Focus Order (Level A)
4. Grouped widgetTab-stop count across the widgetNo criterion of its own; 2.1.1 Keyboard (Level A) applies
5. Dialog opensWhich element received initial focusNo criterion of its own; 2.1.1 Keyboard (Level A) applies
6. Inside the dialogEscape moved focus out, Tab stayed inside while open, behind was obscured2.1.2 No Keyboard Trap (Level A)
7. CloseWhere focus landed after EscapeNo criterion of its own; 2.1.1 Keyboard (Level A) applies

Two more criteria worth knowing while you record#

WCAG 2.2 Success Criterion 2.4.11 Focus Not Obscured (Minimum), at Level AA, requires that a focused component "is not entirely hidden due to author-created content". It is also the only focus-related criterion named here that WCAG 2.2 added at Level AA. In addition, WCAG 2.2 added further Level AA criteria outside this subject. Therefore treat that as a statement about these seven stops and nothing wider.

Separately, WCAG 2.2 Success Criterion 2.4.13 Focus Appearance is Level AAA, not AA.

2.4.13 is worth reading closely if you style focus rings at all. Its exceptions apply in two situations. First, the user agent may determine the indicator, and the author cannot adjust it. Second, the author may not modify the indicator or its background colour. In short, both turn on the author not having touched the indicator. Consequently, once you write a focus style that changes the indicator, neither exception is available to you.

Seven recorded results are not a conformance statement, and running this walk does not establish WCAG conformance. Instead it gives you evidence about seven specific behaviours on one page. In short, that is a smaller claim, and it is one you can defend.

When the same walk is needed across a whole product#

Is the same walk needed across a whole product? Then designing and building to WCAG 2.2 AA from the start is the build-side version of it. The W3C's WCAG 2.2 Recommendation (opens in new tab) defines every criterion named above.

That route is wrong for three situations. A single-page team does not need it. Neither does a team that just ran the walk and recorded seven passes. Nor does a team whose real problem is a design-system upgrade, where the fix belongs in the components rather than on any page.

Keep reading