Spec Integration
Define expected screen reader output in your specs before writing code. Speakable lets you validate that your implementation matches the intended accessibility behavior from day one.
Why define accessibility in specs?
Most accessibility issues are caught late — during QA, audits, or user complaints. By defining expected screen reader output in your component specs, you shift detection to the earliest possible point: before the code is written.
Catch issues at design time
If you can't describe what a screen reader should say, the component likely has an accessibility gap.
Shared understanding
Specs with screen reader output give designers, developers, and QA a concrete definition of “accessible.”
Automated validation
Expected output becomes a test assertion. Regressions are caught automatically in CI.
The spec-first workflow
This workflow integrates Speakable into your development process from the beginning — not as an afterthought.
Define expected output in the spec
Before writing any component code, document what each screen reader should announce for every state (default, disabled, expanded, error, etc.).
Write the HTML/component
Implement the component with the ARIA attributes and semantic HTML needed to produce the expected output.
Validate with Speakable
Run the component HTML through Speakable and compare the actual output against the spec. Fix any mismatches.
Save as baseline
Once the output matches the spec, save the JSON model as a baseline for regression detection.
Automate in CI
Add a CI step that diffs the current output against the baseline. Any unintended changes fail the build.
Writing accessibility specs
An accessibility spec defines the expected screen reader output for a component. You can write these as JSON files, markdown tables, or inline test assertions.
JSON spec format
Define expected output for each screen reader alongside the HTML input:
{
"component": "SubmitButton",
"html": "<button type=\"submit\" aria-label=\"Place order\">Pay $49.99</button>",
"expected": {
"nvda": "Place order, button",
"jaws": "Place order, button",
"voiceover": "Place order, button"
},
"states": [
{
"name": "disabled",
"html": "<button type=\"submit\" aria-label=\"Place order\" disabled>Pay $49.99</button>",
"expected": {
"nvda": "Place order, button, unavailable",
"jaws": "Place order, button, unavailable",
"voiceover": "Place order, button, dimmed"
}
},
{
"name": "loading",
"html": "<button type=\"submit\" aria-label=\"Place order\" aria-busy=\"true\">Processing...</button>",
"expected": {
"nvda": "Place order, button, busy",
"voiceover": "Place order, button, busy"
}
}
]
}Markdown spec format
For design docs and PRDs, use a table format that's readable by non-developers:
## SubmitButton — Accessibility Spec ### Default state | Reader | Expected Output | |------------|------------------------------| | NVDA | "Place order, button" | | JAWS | "Place order, button" | | VoiceOver | "Place order, button" | ### Disabled state | Reader | Expected Output | |------------|----------------------------------------| | NVDA | "Place order, button, unavailable" | | JAWS | "Place order, button, unavailable" | | VoiceOver | "Place order, button, dimmed" | ### Notes - Button uses aria-label to override visible text - Disabled state uses native disabled attribute - VoiceOver says "dimmed" instead of "unavailable"
Validating specs in tests
Turn your specs into automated tests. Each assertion validates that the rendered component matches the expected screen reader output.
React + Vitest
import { render } from '@testing-library/react';
import { parseHTML, buildAccessibilityTree, renderNVDA, renderVoiceOver } from '@reticular/speakable';
import { SubmitButton } from './SubmitButton';
import spec from './submit-button.a11y-spec.json';
describe('SubmitButton accessibility spec', () => {
function getOutput(html: string) {
const doc = parseHTML(html);
const { model } = buildAccessibilityTree(doc.document.body);
return {
nvda: renderNVDA(model),
voiceover: renderVoiceOver(model),
};
}
it('default state matches spec', () => {
const { container } = render(<SubmitButton label="Place order" price="$49.99" />);
const output = getOutput(container.innerHTML);
expect(output.nvda).toContain(spec.expected.nvda);
expect(output.voiceover).toContain(spec.expected.voiceover);
});
it('disabled state matches spec', () => {
const { container } = render(<SubmitButton label="Place order" price="$49.99" disabled />);
const output = getOutput(container.innerHTML);
const disabledSpec = spec.states.find(s => s.name === 'disabled')!;
expect(output.nvda).toContain(disabledSpec.expected.nvda);
expect(output.voiceover).toContain(disabledSpec.expected.voiceover);
});
});CLI-based validation
For simpler workflows, validate specs using the CLI and diff mode:
# Save the spec baseline from your expected HTML
speakable spec/submit-button.html -f json -o spec/submit-button.baseline.json
# After implementation, compare against the spec
speakable dist/submit-button.html --diff spec/submit-button.html
# In CI — fail if output doesn't match spec
speakable dist/submit-button.html --diff spec/submit-button.html || exit 1Real-world spec examples
Here are accessibility specs for common UI patterns. Use these as templates for your own components.
Modal dialog
A dialog with a heading, description, and action buttons.
<div role="dialog" aria-labelledby="title" aria-describedby="desc"> <h2 id="title">Delete account?</h2> <p id="desc">This action cannot be undone.</p> <button>Cancel</button> <button>Delete</button> </div>
"Delete account?, dialog Delete account?, heading level 2 This action cannot be undone. Cancel, button Delete, button"
"web dialog, Delete account? heading level 2, Delete account? This action cannot be undone. Cancel, button Delete, button"
Accordion
An expandable section with aria-expanded toggle.
<button aria-expanded="false" aria-controls="panel1">Shipping info</button> <div id="panel1" hidden> <p>Free shipping on orders over $50.</p> </div>
"Shipping info, button, collapsed"
"Shipping info, button, collapsed"
Search with combobox
A search input with autocomplete suggestions.
<label for="search">Search products</label> <input id="search" type="text" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-controls="results" />
"Search products, combo box, collapsed"
"Search products, combo box, collapsed"
Navigation with current page
A nav landmark with aria-current indicating the active page.
<nav aria-label="Main"> <a href="/" aria-current="page">Home</a> <a href="/products">Products</a> <a href="/about">About</a> </nav>
"Main, navigation landmark Home, link, current page Products, link About, link"
"navigation, Main Home, link, current page Products, link About, link"
Form with validation errors
An input with aria-invalid and an error message linked via aria-describedby.
<label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="err" required /> <span id="err">Please enter a valid email address</span>
"Email, edit, invalid entry, required, Please enter a valid email address"
"Email, edit text, invalid data, required, Please enter a valid email address"
Tips for writing good specs
Spec every interactive state
Default, hover, focus, disabled, loading, error — each state may produce different screen reader output.
Include all three readers
NVDA, JAWS, and VoiceOver announce differently. A spec that only covers one reader misses cross-platform issues.
Write specs before code
If you can't describe what the screen reader should say, the design likely needs accessibility review.
Store specs alongside components
Keep .a11y-spec.json files next to your component source for easy discovery and maintenance.
Use the web analyzer for drafting
Paste your expected HTML into getspeakable.dev/tool to quickly see what each reader would say, then copy the output into your spec.