DEV Community

Cover image for DOM order is not always keyboard order — I built a Chromium scanner to visualize it
Damiano Ciarla
Damiano Ciarla

Posted on

DOM order is not always keyboard order — I built a Chromium scanner to visualize it

The DOM can tell you where elements are. It cannot always tell you the exact
route a keyboard user will experience.

Positive tabindex values can reorder controls. JavaScript can redirect focus.
Dialogs can fail to restore it. A custom element may contain an open, closed, or
cross-origin boundary. A scroll container can move the active control outside
the evidence captured in the first viewport.

Static rules are essential, but keyboard navigation is also an interaction. I
wanted a way to observe that interaction and preserve what happened.

So I built FocusPath, an open-source TypeScript tool that follows Tab or
Shift+Tab navigation in a real Chromium session, records the observable focus
stops, and generates a portable HTML report with the route drawn over the page.

npx focuspath https://example.com
Enter fullscreen mode Exit fullscreen mode

The live beta is available at
damianociarla.github.io/focuspath,
and the source is on
GitHub.

FocusPath live scanner showing the completed route for Example Domain

The question FocusPath answers

FocusPath does not ask only, "Which elements look focusable?"

It asks:

Starting from this document, what focus movement can Chromium actually
observe when a keyboard user presses Tab?

For every observed stop, the report records information such as:

  • sequence number and total Tab presses;
  • element identity, selector, role, and computed accessible name;
  • traversal-time and final screenshot geometry;
  • scroll and clipping contexts;
  • computed outline and shadow values;
  • deterministic findings linked back to the relevant stop.

The report also explains why traversal ended: the document was exhausted,
focus stopped moving, a cycle appeared, or a configured safety budget was
reached.

Why not derive the route from the DOM?

DOM analysis is useful, but several important cases are dynamic.

Imagine an icon-only button after a custom menu. The menu intercepts Tab,
moves focus internally, and later sends focus to an element outside its DOM
subtree. Sorting elements by source position or tabindex does not reproduce
that behavior.

Closed shadow roots and cross-origin frames add another constraint: an external
scanner cannot inspect their internal controls. FocusPath treats these as
opaque boundaries and reports only what can be observed from the host page. It
does not invent internal evidence.

That distinction became one of the central design rules of the project:

Missing evidence must remain missing evidence, not become a confident guess.

A report should preserve its limits

Visual evidence is persuasive, which also makes it easy to overstate.

FocusPath reports include the engine, direction, viewport, traversal budgets,
request totals, blocked resource types, capture dimensions, and truncation
state. A screenshot that does not cover the complete source document is marked
as truncated. A stop outside captured evidence remains in the sequence instead
of being silently dropped.

The scanner and reporter share a hard evidence budget. Saved reports are
runtime-validated before rendering, including counters, step references,
geometry, capture metadata, and JPEG dimensions.

These constraints make the output less magical and more reviewable.

FocusPath complements rule engines

FocusPath is not a replacement for axe, Lighthouse, Accessibility Insights,
screen-reader testing, or manual review.

Those tools answer different questions. Rule engines are excellent at finding
many deterministic violations in page state. FocusPath concentrates on the
route produced by keyboard traversal and the evidence surrounding it.

A practical workflow can use both:

  1. Run established automated rules.
  2. Generate a FocusPath report for important journeys.
  3. Inspect the route and any deterministic focus findings.
  4. Test manually across browsers and assistive technologies.
  5. Review focus appearance, content, and interaction semantics with people.

No automated tool, including FocusPath, certifies WCAG conformance.

Running it locally

FocusPath is ESM-only and currently targets Node.js 24 or newer.

# Create focuspath-report.html
npx focuspath https://example.com

# Use a mobile viewport
npx focuspath localhost:3000 --viewport 390x844

# Inspect reverse traversal
npx focuspath example.com --direction reverse

# Bound traversal explicitly
npx focuspath example.com \
  --max-steps 80 \
  --max-tab-presses 320 \
  --max-opaque-tab-presses 160
Enter fullscreen mode Exit fullscreen mode

The CLI exits with code 1 for an error finding, 2 when scanning fails, and
0 otherwise, so it can participate in scripted workflows without pretending
that every warning should block a build.

The library API is available too:

import { scanFocusPath } from "focuspath";
import { generateHtmlReport } from "focuspath/reporter";

const report = await scanFocusPath("http://localhost:3000", {
  viewport: { width: 1440, height: 900 },
  direction: "reverse",
  maxSteps: 60,
  maxTabPresses: 240,
});

const html = generateHtmlReport(report);
Enter fullscreen mode Exit fullscreen mode

The separate reporter entry point does not load Playwright, which keeps
report-only consumers smaller and easier to deploy.

The hosted scanner is intentionally narrower

The public beta accepts public HTTP(S) pages and runs one bounded, forward-only
scan. It uses stricter limits than the local CLI, blocks font and media requests
to control resource use, and discloses those restrictions in every report.

Submitted URLs are validated before scanning, Chromium traffic passes through
a pinned egress proxy, and private, loopback, link-local, and cloud-metadata
destinations are rejected. The service intentionally has no application-level
report store, although infrastructure providers may retain request metadata.

The local package remains the right choice for private projects, configurable
limits, reverse traversal, and sensitive pages.

What I would like feedback on

The current release focuses on deterministic traversal and portable evidence.
The roadmap includes:

  • a GitHub Action with SARIF annotations;
  • comparison between two focus paths;
  • focus restoration after dialogs close;
  • Firefox and WebKit coverage.

I am especially interested in pages with complex menus, dialogs, scroll
containers, custom elements, and embedded content. If the report makes an
observable boundary or stop unclear, that is useful feedback.

Try the live scanner, inspect the
source, or install
focuspath from npm.

The goal is not to replace human accessibility judgment. It is to make one
important part of the interaction easier to see, reproduce, and discuss.

Top comments (5)

Collapse
 
marlenesco profile image
David Foliti •

Really interesting approach. I especially like the distinction between deriving focus order from the DOM and actually observing what Chromium does at runtime.

The idea that “missing evidence must remain missing evidence” is also a strong design choice, particularly with closed shadow roots, cross-origin frames, and JS-driven focus management.

The focus-path comparison on the roadmap sounds especially useful. Being able to diff keyboard navigation between two versions of a page could make this a very practical regression-testing tool in CI.

Nice work, definitely one to keep an eye on

Collapse
 
micaavigliano profile image
Mica •

you can easily test the focus order by just pressing Tab

Collapse
 
damianociarla profile image
Damiano Ciarla •

Absolutely—pressing Tab is still the essential manual test, and FocusPath is not intended to replace it.

Its value is in preserving what happened as repeatable evidence: the observed sequence, accessible names and roles, geometry, scroll contexts, focus styles, and deterministic findings. That evidence can then be shared, reviewed, compared between versions, or used in automated workflows.

A human still needs to decide whether the resulting experience makes sense. I see FocusPath as a recorder and evidence layer for keyboard testing, not a substitute for it.

Collapse
 
bertrandmorel profile image
Bertrand Morel •

The most useful boundary here is between a traversal report and a conformance claim. An observed Chromium route gives reproducible evidence for one engine, viewport, document state, and direction; it does not describe every path a keyboard user may encounter.

The next challenge may be state rather than direction. A single forward traversal describes one page state, but menus, comboboxes, dialogs, disclosures, and validation errors create new focus graphs after Enter, Space, Escape, or an asynchronous update. I could imagine a journey mode that records both the action that changed state and the subsequent focus path, then verifies restoration when the transient UI closes.

Pairing each stop with the computed accessible name is already valuable. Would you consider also preserving a small accessibility-tree snapshot or state diff at each transition? That could help a regression report explain not only where focus moved, but whether the focused object still exposed the intended role, name, and state.

Collapse
 
damianociarla profile image
Damiano Ciarla •

This is a very useful framing—the next dimension is state, not simply another traversal direction.

I would be cautious about preserving a complete accessibility tree at every stop because it could make reports large, noisy, and expose more page content than necessary. A bounded partial AX snapshot for the focused object—role, name, description, value, and relevant states such as expanded, checked, selected, disabled, or invalid—plus a semantic diff between transitions seems more reviewable.

For journey mode, I’m leaning toward explicit scripted actions rather than automatically exploring every possible interaction, so that the evidence remains deterministic and reproducible. Dialog opening, closing, and focus restoration would be a strong first use case.

In your experience, would a focused-object snapshot be sufficient, or are parent/context nodes usually necessary to explain meaningful regressions?