Check the tab order in a real browser, not a guess
Out of Order works out the exact path the Tab key takes through a page, then checks that the order is right and that every stop is reachable, visible, and announced. It runs in real Chromium only, because that is the one place the answer is true.
See it on the page with trace().
Check it in a test or CI with audit().
Scan any URL from the terminal with the out-of-order CLI,
and pipe the findings into an AI agent.
What it checks
By default Out of Order runs headless: it analyzes the page and returns a plain result.
The trace() overlay is an optional layer on top, for when you want to see the
tab path with your own eyes. The analysis itself treats a tab order as more than a list.
It asks four questions, and every rule answers exactly one.
-
Order is about whether focus follows the reading order, or hops around
because of a positive
tabindexor a mismatch between the DOM and the layout. - Reachability is about whether the keyboard can get to every interactive control, and whether it avoids the dead stops it should not land on.
- Visibility is about whether every tab stop is actually painted, or is tabbable while invisible because of opacity, an off-screen position, or clipping.
-
Announcement is about whether every stop has an accessible name, and
whether it is free of the
aria-hiddenand nesting traps that a screen reader chokes on.
Each rule is grounded in a specific WCAG, WAI-ARIA, or HTML-spec clause. The rules page lists all of them with the clause and a live example.
A drop-in overlay
The same analysis powers a live overlay you can drop on any page. It numbers the real tab path and draws the hops between stops, turning a hop red when it runs against reading order.
import { trace } from "@out-of-order/trace";
// No root, so it analyzes the whole document, exactly like tabbing the page.
const overlay = trace();
That is the whole integration. The overlay re-analyzes itself on every DOM mutation. The two demo pages are nothing but this call.
Why real-browser only
Any honest focus check needs CSS layout. It has to know what is really visible, what is
clipped, and where each control sits on screen.
jsdom has no layout engine, so a green jsdom test cannot promise
the order holds up in a real browser. Out of Order reads live visibility and bounding
rects, so a passing check reflects what a keyboard user will actually get.
What it can't see
A check reads the page as it is the instant it runs. It sees the live DOM and its computed
styles, including styles behind state selectors like :focus, but it never
fires events or runs the page's own JavaScript. Handlers bound with
addEventListener (what Vue @click, React onClick,
and most frameworks compile to) leave no DOM trace, so a bare
<div @click> with no role is invisible to it. Give custom controls a
real role or a native element.
So state a script only produces at runtime is invisible to it. If a handler reveals a hidden element the moment it is tabbed to, the check sees only the resting state and can flag the element as invisible even though a keyboard user would be fine. When that happens, drive the interaction yourself, in a test or by hand, and run the check again on the state you actually get. A finding you have already weighed can be approved in place.
A few things sit in the tab order in real browsers but are left out on purpose, because
they cannot be reasoned about across browsers. One is an <iframe>,
since its own document owns its stops. Another is a keyboard-focusable scroll container,
since browsers disagree on it.
The packages
A small monorepo. The analyzer is pure and framework-free. Everything visual is a separate layer on top of it.
| Package | What it is | Reach for it when |
|---|---|---|
@out-of-order/core |
The analyzer. It wraps tabbable, applies the rules, and returns a plain
result. No framework or test-runner deps.
API →
|
You assert in a test or CI, or run headless in a Playwright
page.evaluate, with no UI to draw.
|
@out-of-order/trace |
The framework-agnostic live overlay, built on the analyzer. API → | You want to see the tab path and findings on a live page. This is the one most people want. |
@out-of-order/vitest |
A toHaveValidTabOrder() matcher for Vitest Browser Mode, built on the
analyzer. Released, but the API is still settling.
API →
|
You already run Vitest Browser Mode and want the check as a one-line assertion in your component tests. |
@out-of-order/cli |
Audits any URL from the terminal. Launches a headless browser, runs the analyzer,
and prints findings to stdout, with an exit code that fails on violations. Add
--overlay to open a real browser with the overlay drawn on the live
page.
|
You want to check a live URL without writing code, gate CI on the exit code, or pipe findings straight into an AI agent. |
A Playwright adapter, sharing the same core, is in progress.