Runtime Accessibility Testing in the Browser
Runtime accessibility testing in a real browser lets you check what screen readers announce for any component, including web components built with Lit or custom elements, without spinning up Storybook. Speakable ships two transport-agnostic entry points for this: a browser bundle that analyzes live DOM in the current page, and an iframe harness that mounts a component in isolation and drives analysis over postMessage. Both run the same engine as the CLI and the Storybook addon, so output stays consistent across every surface.
When to Use the Browser Bundle or Iframe Harness
Speakable has several runtime surfaces. Pick the one that matches where your component lives:
| Surface | Runs in | Best for |
|---|---|---|
| Storybook addon | Storybook preview iframe | Component-driven development with Timeline and Diff tabs |
| Browser bundle | The current page (real browser) | Analyzing live DOM in your own app, test runner, or e2e page |
| Iframe harness | An isolated iframe you control | Testing a component in isolation, including web components and shadow DOM |
| CLI runtime | Node plus a headless browser | Whole-library regression runs in CI |
For the Storybook path, see the Storybook addon guide. For the CLI path, see Runtime Analysis.
How to Analyze Live DOM with the Browser Bundle
The browser bundle is exported at @reticular/speakable/browser. It runs against live DOM in the current page, so it captures JavaScript-set state and open shadow roots that a serialized HTML string would lose. Use it inside a test runner (Vitest, Playwright, Web Test Runner) or anywhere you already have a live element.
Static analysis with analyzeElement
analyzeElement returns per-reader output, audit findings, structural stats, and warnings. It never throws on empty or detached input:
import { analyzeElement } from '@reticular/speakable/browser';
const result = analyzeElement(document.querySelector('#widget'));
console.log(result.nvda); // ["Save changes, button", ...]
console.log(result.audit); // [{ severity, message, selector }, ...]
console.log(result.stats); // { totalElements, interactiveElements, ... }
console.log(result.warnings);For web components that hydrate asynchronously, use analyzeElementWithUpgrade. It awaits custom-element upgrade first, then folds any upgrade warnings into the result:
import { analyzeElementWithUpgrade } from '@reticular/speakable/browser';
const result = await analyzeElementWithUpgrade(
document.querySelector('my-widget')
);Capture an interaction timeline with captureTimeline
captureTimeline attaches the runtime engine to a live document, runs an interaction sequence, and returns a serializable timeline of focus moves, state changes, and announcements:
import { captureTimeline } from '@reticular/speakable/browser';
const timeline = await captureTimeline(document, {
componentName: 'Menu',
sequence: {
description: 'open and arrow down',
actions: [
{ type: 'click', selector: '#menu-btn' },
{ type: 'arrowDown' },
],
},
});
for (const event of timeline.events) {
console.log(event.type, event.target.accessibleName);
}See the API reference for the full AnalysisResult shape and CaptureOptions.
How to Test a Component in Isolation with the Iframe Harness
The iframe harness, exported at @reticular/speakable/harness, mounts a component into an iframe from a URL or an HTML string, injects the browser bundle, and drives analysis over postMessage. Because it runs in a real browser, open shadow roots and slot projection work correctly, and custom elements are awaited before analysis. This is the way to test any component in isolation without Storybook.
Mount, analyze, and capture
import { createHarness } from '@reticular/speakable/harness';
const harness = createHarness({
target: { container: document.body }, // harness creates + owns the iframe
bundleUrl: '/speakable-browser.global.js', // the injectable IIFE bundle
});
// Mount a component (HTML string via srcdoc, or a same-origin URL)
await harness.load({ html: '<my-widget>Content</my-widget>' });
// Static analysis, scoped to a selector
const result = await harness.analyze('my-widget');
// Run an interaction sequence and get the timeline
const timeline = await harness.captureTimeline({
componentName: 'MyWidget',
sequence: {
description: 'toggle',
actions: [{ type: 'click', selector: 'my-widget' }],
},
});
harness.destroy();Where to get the injectable bundle
The harness injects an IIFE build named speakable-browser.global.js, shipped in the package's dist/. Serve it from your app or copy it to a static path, then pass its URL as bundleUrl. If the bundle is already present in the iframe, you can omit bundleUrl.
Same-origin content only
The harness supports HTML strings (mounted via srcdoc) and same-origin URLs. Cross-origin URLs are rejected with a descriptive error, because a script cannot be injected across origins. To test a cross-origin component, embed the bundle in that page and omit bundleUrl.
Testing Web Components and Shadow DOM
Both the browser bundle and the harness run in a real browser, so they handle web component internals that a static Node analysis cannot:
- Open shadow roots are traversed, so content rendered inside a custom element's shadow tree is analyzed.
- Slot projection is resolved, so light-DOM children projected into a
<slot>appear in the correct shadow-tree order. - Custom-element upgrade is awaited via
customElements.whenDefinedand, where present, a Lit-styleupdateCompletepromise, so components are analyzed after hydration. - Closed shadow roots cannot be traversed by any tool. Speakable surfaces a warning so partial coverage is not mistaken for full coverage.
Upgrade waiting is bounded by a timeout, so analysis never hangs. If a component does not upgrade in time, a warning is added to result.warnings and analysis proceeds.
Related Pages
Storybook Addon
Timeline and Diff tabs, per-reader predictions, and parameters.speakable for component-driven development.
Runtime Analysis
The CLI runtime engine, built-in interaction patterns, and Storybook regression runs in CI.
API Reference
Full signatures for the browser and harness modules, including AnalysisResult and CaptureOptions.
Component Patterns
Accessible patterns for dialogs, comboboxes, tabs, and more, with predicted screen reader output.