JavaScript API
init, scan, show, dock, hide — and what each of them costs.
On an unlocked page the global LiveAudit exists. Nothing starts by itself: the WebAssembly
module loads on the first init(), the layer appears on the first show().
await LiveAudit.show(); // scan the document, draw the layer
await LiveAudit.show(element); // scan a subtree instead
await LiveAudit.show(undefined, { dock: 'left' }); // and dock the sidebar left
LiveAudit.hide(); // remove it; nothing stays behind
scan(root?, options?)
Scans and returns the result without drawing anything — for your own UI, or for reading the findings in the console.
const result = await LiveAudit.scan(document.body);
result.documents[0].report.findings; // Finding[]
result.documents[0].nodes; // nodes collected
Each same-origin <iframe> is scanned as its own document with its own id space and appears
as a further entry in documents. A cross-origin frame cannot be entered and is reported as
an untested scope — not as a silent omission.
show(root?, options?)
Scans, then draws the layer. options are those of scan, plus dock for the edge the
sidebar attaches to (right, left, top, bottom; the choice is remembered).
The contrast pass: { rendering: true }
Contrast needs computed styles and the effective background behind the text, which is a second walk over the collected elements. It costs 1.9× to 4.6× the collector, so it is not part of every scan:
await LiveAudit.show(undefined, { rendering: true });
Without it the contrast rules do not run; the report lists them as not run, with the reason —
never as PASS, and never silence.
The same pass feeds heuristic checks: CSS reordering over focusable content, endless
animation without a pause control, min-width above 320 px, elements that look clickable
but have no role, controls hidden under fixed bars, targets under 24 × 24 px that also miss
the spacing exception. They report REVIEW, never FAIL — a place to look, not a verdict.
Focus visibility: { rendering: true, focus: true }
Whether keyboard focus is visible can only be measured by focusing. This pass focuses every
reachable control once (focusVisible: true, preventScroll), compares outline, shadow,
border, background and text before and after, and puts focus back where it was:
await LiveAudit.show(undefined, { rendering: true, focus: true });
It is the one pass that changes the page’s state: the page receives focus and blur and may
react to them. Without it, focus visibility is reported as UNTESTED.
The checklist: manual/*
What no machine can decide — captions and transcripts, whether an alt text fits, text
styled as a heading, colour as the only cue, time limits, error messages, captchas — appears
once per page as UNTESTED whenever the page contains something it applies to. It is the
list of what still needs a person.
Live mode: watch(root?, options?)
Scans, draws the layer, and keeps it current while the page changes — for pages that build themselves after load, or that you are editing while you look at them.
await LiveAudit.watch(); // stays current
await LiveAudit.watch(undefined, { debounceMs: 500 });
LiveAudit.unwatch(); // stop; the layer stays as it is
Only the changed subtree is scanned again, 200 ms after the last mutation and at
most 1 s after the first (maxWaitMs), so a page that never settles still updates.
Collecting the DOM is the measured bottleneck — about 90% of a scan — so a full
rescan per mutation would be felt on any page that moves.
Two limits, both deliberate:
- Rules that speak about the scope as a whole — a missing
mainlandmark, a missing title, a missingh1— stay as the last full scan left them. A subtree is not a document, and answering those questions on a fragment would invent a verdict. They are as old as the last full scan, which beats being wrong. - Mutations inside a same-origin frame are not watched. Frames are rescanned when a subtree above them changes.
State that changes without a mutation — :checked, :has(), custom properties — is picked
up on change (the whole observed root) and transitionend (the element that moved). For
anything else, rescan yourself:
await LiveAudit.rescan(); // in live mode: the observed root
await LiveAudit.rescan(panel); // in live mode: just this subtree
Without live mode, rescan() repeats the last show().
Unlocking from your own code
import LiveAudit, { enable } from '/vendor/liveaudit/inspector.js';
enable(); // programmatic unlock, bypassing the ?liveaudit flag
enable() is what this site’s own demo page uses: the page is the tool’s purpose, so it says
so in code instead of relying on a query parameter.
Others
| Call | Does |
|---|---|
init() |
Loads and instantiates the WebAssembly module; repeated calls share one load |
isVisible() |
Whether the layer is currently in the document |
dock(side) |
Moves the sidebar without scanning again |
remember() / forget() |
Keeps the unlock for this origin, or drops it |