Skip to main content
Skip to docs content

Framework Guides

Integrate Speakable into your React, Angular, Svelte, or Web Component workflow. Each guide shows how to extract rendered HTML from your components and feed it into Speakable for screen reader simulation.

React

Use @testing-library/react to render components, then pass the HTML to Speakable for analysis.

Unit test integration

Render your component in a test, extract the HTML, and run Speakable programmatically:

button.a11y.test.tsx
import { render } from '@testing-library/react';
import { parseHTML, buildAccessibilityTree, renderNVDA } from '@reticular/speakable';
import { SubmitButton } from './SubmitButton';

test('button announces correctly for NVDA', () => {
  const { container } = render(<SubmitButton label="Place Order" />);
  const html = container.innerHTML;

  const doc = parseHTML(html);
  const { model } = buildAccessibilityTree(doc.document.body);
  const output = renderNVDA(model);

  expect(output).toContain('Place Order, button');
});

Storybook integration

Export rendered HTML from Storybook stories and analyze with the CLI:

Terminal
# Build Storybook static HTML, then analyze
npx storybook build -o storybook-static
speakable storybook-static/iframe.html --selector ".sb-show-main" -f audit

Next.js pages

For Next.js, build the site and analyze the static HTML output:

Terminal
next build
speakable out/index.html -f audit -s all

# Analyze a specific page
speakable out/pricing.html --selector "main" -f text
A

Angular

Use Angular's TestBed to render components, then extract the native element HTML for analysis.

Component test integration

button.a11y.spec.ts
import { TestBed } from '@angular/core/testing';
import { parseHTML, buildAccessibilityTree, renderNVDA } from '@reticular/speakable';
import { SubmitButtonComponent } from './submit-button.component';

describe('SubmitButton a11y', () => {
  it('announces correctly for NVDA', () => {
    TestBed.configureTestingModule({
      declarations: [SubmitButtonComponent]
    });
    const fixture = TestBed.createComponent(SubmitButtonComponent);
    fixture.componentInstance.label = 'Place Order';
    fixture.detectChanges();

    const html = fixture.nativeElement.innerHTML;
    const doc = parseHTML(html);
    const { model } = buildAccessibilityTree(doc.document.body);
    const output = renderNVDA(model);

    expect(output).toContain('Place Order, button');
  });
});

Build output analysis

Terminal
ng build
speakable dist/my-app/browser/index.html -f audit
S

Svelte

Use @testing-library/svelte to render components, then pass the HTML to Speakable. For SvelteKit apps, analyze the static adapter output directly.

Component test integration

Render your Svelte component in a test and extract the HTML for analysis:

SubmitButton.a11y.test.ts
import { render } from '@testing-library/svelte';
import { parseHTML, buildAccessibilityTree, renderNVDA, renderVoiceOver } from '@reticular/speakable';
import SubmitButton from './SubmitButton.svelte';

test('button announces correctly for NVDA', () => {
  const { container } = render(SubmitButton, {
    props: { label: 'Place Order' }
  });
  const html = container.innerHTML;

  const doc = parseHTML(html);
  const { model } = buildAccessibilityTree(doc.document.body);

  expect(renderNVDA(model)).toContain('Place Order, button');
});

test('VoiceOver announces role first for landmarks', () => {
  const { container } = render(NavBar, {
    props: { label: 'Main' }
  });
  const doc = parseHTML(container.innerHTML);
  const { model } = buildAccessibilityTree(doc.document.body);

  // VoiceOver puts role before name for landmarks
  expect(renderVoiceOver(model)).toContain('navigation, Main');
});

SvelteKit static output

If you're using SvelteKit with the static adapter, analyze the build output directly:

Terminal
# Build with static adapter
npm run build

# Analyze the generated pages
speakable build/index.html -f audit -s all

# Analyze a specific route
speakable build/about/index.html --selector "main" -f text

# Batch analyze all pages
speakable --batch build/**/index.html -f audit

Svelte 5 with runes

The same pattern works with Svelte 5 — the testing library renders the component to DOM regardless of whether you use runes or legacy syntax:

Dialog.a11y.test.ts
import { render } from '@testing-library/svelte';
import { parseHTML, buildAccessibilityTree, renderNVDA } from '@reticular/speakable';
import Dialog from './Dialog.svelte';

test('dialog announces with title and description', () => {
  const { container } = render(Dialog, {
    props: {
      open: true,
      title: 'Delete account?',
      description: 'This cannot be undone.'
    }
  });

  const doc = parseHTML(container.innerHTML);
  const { model } = buildAccessibilityTree(doc.document.body);
  const output = renderNVDA(model);

  expect(output).toContain('Delete account?, dialog');
  expect(output).toContain('This cannot be undone.');
});
WC

Web Components

Web Components render to the light DOM or shadow DOM. Speakable analyzes the light DOM output — extract innerHTML or use shadowRoot.innerHTML for shadow DOM components.

Lit component testing

my-button.a11y.test.ts
import { fixture, html } from '@open-wc/testing';
import { parseHTML, buildAccessibilityTree, renderNVDA } from '@reticular/speakable';
import './my-button.js';

it('announces correctly', async () => {
  const el = await fixture(html`<my-button label="Submit"></my-button>`);

  // For light DOM components
  const output = el.innerHTML;

  // For shadow DOM components
  // const output = el.shadowRoot.innerHTML;

  const doc = parseHTML(output);
  const { model } = buildAccessibilityTree(doc.document.body);
  expect(renderNVDA(model)).toContain('Submit, button');
});

Vanilla Web Components

test.js
import { parseHTML, buildAccessibilityTree, renderVoiceOver } from '@reticular/speakable';

// Create and render the component
const el = document.createElement('my-dialog');
el.setAttribute('open', '');
el.innerHTML = '<h2>Confirm</h2><button>OK</button>';
document.body.appendChild(el);

// Analyze
const doc = parseHTML(el.outerHTML);
const { model } = buildAccessibilityTree(doc.document.body);
console.log(renderVoiceOver(model));
// "web dialog
  heading level 2, Confirm
  OK, button"

General Pattern

Regardless of framework, the integration pattern is the same:

  1. Render your component to HTML (via test harness, build output, or DOM API)
  2. Pass the HTML string to parseHTML()
  3. Build the accessibility tree with buildAccessibilityTree()
  4. Render with your target screen reader (renderNVDA, renderJAWS, renderVoiceOver)
  5. Assert against expected output or save as a baseline for regression detection

The CLI works with any static HTML file. If your framework produces HTML output (SSG, SSR, or build artifacts), you can analyze it directly without writing any integration code: speakable dist/index.html -f audit