Skip to main content
Skip to docs content

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.

1

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.).

2

Write the HTML/component

Implement the component with the ARIA attributes and semantic HTML needed to produce the expected output.

3

Validate with Speakable

Run the component HTML through Speakable and compare the actual output against the spec. Fix any mismatches.

4

Save as baseline

Once the output matches the spec, save the JSON model as a baseline for regression detection.

5

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:

submit-button.a11y-spec.json
{
  "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:

submit-button.spec.md
## 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

submit-button.a11y.test.tsx
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:

Terminal
# 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 1

Real-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.

HTML
<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>
NVDA
"Delete account?, dialog
  Delete account?, heading level 2
  This action cannot be undone.
  Cancel, button
  Delete, button"
VoiceOver
"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.

HTML
<button aria-expanded="false" aria-controls="panel1">Shipping info</button>
<div id="panel1" hidden>
  <p>Free shipping on orders over $50.</p>
</div>
NVDA
"Shipping info, button, collapsed"
VoiceOver
"Shipping info, button, collapsed"

Search with combobox

A search input with autocomplete suggestions.

HTML
<label for="search">Search products</label>
<input id="search" type="text" role="combobox"
  aria-expanded="false" aria-autocomplete="list"
  aria-controls="results" />
NVDA
"Search products, combo box, collapsed"
VoiceOver
"Search products, combo box, collapsed"

Navigation with current page

A nav landmark with aria-current indicating the active page.

HTML
<nav aria-label="Main">
  <a href="/" aria-current="page">Home</a>
  <a href="/products">Products</a>
  <a href="/about">About</a>
</nav>
NVDA
"Main, navigation landmark
  Home, link, current page
  Products, link
  About, link"
VoiceOver
"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.

HTML
<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>
NVDA
"Email, edit, invalid entry, required,
  Please enter a valid email address"
VoiceOver
"Email, edit text, invalid data, required,
  Please enter a valid email address"

Tips for writing good specs

1.

Spec every interactive state

Default, hover, focus, disabled, loading, error — each state may produce different screen reader output.

2.

Include all three readers

NVDA, JAWS, and VoiceOver announce differently. A spec that only covers one reader misses cross-platform issues.

3.

Write specs before code

If you can't describe what the screen reader should say, the design likely needs accessibility review.

4.

Store specs alongside components

Keep .a11y-spec.json files next to your component source for easy discovery and maintenance.

5.

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.