DEV Community

Cover image for Non-functional Application Requirements: Accessibility
Oluwasegun Adedigba
Oluwasegun Adedigba

Posted on Edited on

Non-functional Application Requirements: Accessibility

On 4 June 2026, a court in Caen, France, ordered Carrefour to make its website and app fully accessible to disabled customers within six months, with a penalty for every day it runs late. Two disability associations, apiDV and Droit Pluriel, brought the case under the French law that carries the European Accessibility Act.

Carrefour's defense was a number. It said the site already met 71% of the accessibility criteria. The judge said it had to be totally accessible (Deque, June 2026).

Seventy-one percent sounds like a pass. For a blind shopper who can't get through checkout, it's a locked door.

This part takes six popular myths about accessibility, puts each one against the evidence, and then builds the CI gate that stops you shipping your own 71%.

TL;DR

  • Aim for WCAG 2.2 Level AA. Meeting 2.2 also meets 2.0 and 2.1, which most laws still name.
  • Six boring error types made up 96% of what WebAIM found in 2026. Native HTML prevents most of them for free.
  • Gate CI on axe-core, add role-based Playwright tests, and keep a human screen reader pass, because a clickable div sails through axe.
  • The law is live: the EAA since 28 June 2025, the US ADA Title II rule from 26 April 2027.
  • Streaming LLM answers should be announced in sentences, not tokens. The same markup serves screen readers and AI browser agents.

What: accessibility, defined precisely

Accessibility means people with disabilities can perceive, understand, operate and navigate your product, with or without assistive technology like screen readers, magnifiers or voice control. Don't confuse it with conformance, which is passing a standard's testable criteria. Conformance is the measurable proxy. Accessibility is the outcome, and you can have the first without the second.

The measurable form is the Web Content Accessibility Guidelines (WCAG). Version 2.2 became a W3C Recommendation in 2023 and the international standard ISO/IEC 40500:2025 in October 2025. Its criteria come in levels A, AA and AAA, and when a law says "accessible", it almost always means AA.

Scope: web content, and in most legal readings apps and documents too.

What 2.2 adds at A and AA:

  • 2.4.11 Focus Not Obscured (AA): sticky headers can't completely hide the focused element.
  • 2.5.7 Dragging Movements (AA): anything you drag also works with a single tap.
  • 2.5.8 Target Size (AA): pointer targets of at least 24 by 24 CSS pixels, or enough spacing.
  • 3.2.6 Consistent Help (A): help links stay in the same place.
  • 3.3.7 Redundant Entry (A): don't make people retype what they gave you.
  • 3.3.8 Accessible Authentication (AA): no memory test or puzzle to sign in. Paste, password managers and passkeys satisfy it.

A testable requirement reads: "Every route passes automated WCAG 2.2 AA checks on every pull request, sign-in and checkout complete with a keyboard and a screen reader, and no pointer target is under 24 by 24 CSS pixels."

Why: who you're leaving out, and who's watching

About 1.3 billion people, 16% of the world's population, live with a significant disability (WHO, 2023). Add the temporary and situational cases: a broken wrist, glare on a phone, a supervisor on a loud factory floor who can't hear an alert.

The web mostly fails them. WebAIM's 2026 scan of the top million home pages found detectable WCAG failures on 95.9% of them, up from 94.8% and reversing six years of slow improvement. The average page had 56.1 errors (WebAIM Million, 2026).

Then there's the law, which is where accessibility starts to look like Part 3 on Compliance: a regulation, a standard it points to, and evidence you produce on demand. Myth 5 has the dates.

Six myths, checked against the evidence

Six accessibility myths and what the evidence says. Accessibility is for a small group of users: one in six people has a disability, and captions and focus rings help everyone. It is expensive and slows us down: six common failures make up 96% of errors on home pages (WebAIM, 2026). A scanner or an overlay makes us compliant: ten free checkers together found 71% of 143 known barriers (GOV.UK, 2017). More ARIA makes it more accessible: pages using ARIA averaged 59.1 errors against 42 without (WebAIM, 2026). We are not in the EU, so the law cannot reach us: the European Accessibility Act has covered products and services sold to EU consumers since 28 June 2025. AI makes accessibility free: the US Justice Department says AI does not yet reliably fix inaccessible content at scale.

Myth 1: "Accessibility is for a small group of users"

One in six people isn't a niche, and it undercounts your users. Captions get used in noisy rooms. A visible focus ring helps anyone fighting a bad trackpad.

There's a newer group of users, too. Screen readers, voice control, your Playwright tests and AI browser agents all read the same thing: the accessibility tree the browser builds from your markup. OpenAI's developer FAQ says "making your website more accessible helps ChatGPT Agent in Atlas understand it better". Microsoft's Playwright MCP server drives a browser for an LLM from that tree, not from screenshots.

flowchart LR
  M["Your HTML<br/>and ARIA"] --> T["Browser accessibility tree<br/>roles, names, states"]
  T --> SR["Screen readers"]
  T --> VC["Voice control"]
  T --> PT["Playwright<br/>getByRole tests"]
  T --> AG["AI browser agents"]

Every one of these reads the same tree, so one missing button name breaks all of them at once.

Myth 2: "It's expensive and slows us down"

Here are WebAIM's most common failures in 2026, by share of home pages: low contrast text (83.9%), missing alt text (53.1%), missing form labels (51%), empty links (46.3%), empty buttons (30.6%) and no page language (13.5%). Those six made up 96% of every error detected.

Each one is a line of code. A checked color token, an alt attribute, a <label for>, text in a link, a name on an icon button, lang="en". None of that slows a sprint, and native <button> and <a href> bring keyboard support and the right role for free.

The real cost lives in two places: retrofitting, which Carrefour now does on a judge's clock, and custom widgets like comboboxes and data grids. For those I'd use a tested library such as React Aria, which ships over 40 unstyled components tested with screen readers, rather than build my own. You still test what you build with it.

Myth 3: "A scanner, or an overlay, makes us compliant"

Automated tools are necessary and nowhere near enough. In 2017 the UK government built a page with 143 deliberate barriers and ran ten free checkers on it. Together they found 71%, and 42 barriers got past every tool (GOV.UK, 2017). Deque, which makes axe, puts its own coverage at 57% of issues by volume (2021), a generous way of counting that still leaves four in ten for people.

Here's what that looks like. A sign-in page with a <div onclick> reading "Continue" passed axe-core 4.14 with zero violations. A Playwright test looking for a button named "Continue" failed immediately.

flowchart LR
  D["div onclick<br/>with text Continue"] --> AX["axe-core scan"]
  AX -->|"no rule fails"| G["Green build"]
  D --> SR["Screen reader<br/>and keyboard"]
  SR -->|"read as plain text<br/>skipped by Tab"| X["User stuck<br/>at sign-in"]
  D --> RT["getByRole button test"]
  RT -->|"button not found"| R["Red build"]

The same clickable div passes the scanner and fails both the user and a role-based test.

Overlays, the one-script-tag fixes, fail the same way at a higher price. In April 2025 the US Federal Trade Commission finalized a $1 million order against accessiBe, saying its AI-powered widget did not make all customer sites WCAG-compliant, and barred it from claiming otherwise without evidence (FTC, 2025).

Myth 4: "More ARIA makes it more accessible"

In WebAIM's 2026 data, pages using ARIA averaged 59.1 errors against 42 without. WebAIM calls that a correlation, since ARIA-heavy pages are also more complex. It still matches the W3C's first rule of ARIA: if a native element already does what you need, use it.

ARIA changes what assistive technology is told. It doesn't add behavior. role="button" on a div won't make it focusable or make Enter work.

Agents add a twist. That OpenAI FAQ talks about "ARIA tags", and accessibility consultant Adrian Roselli replied that native HTML is the right advice and that ARIA labels will get keyword-stuffed like alt text was. Keep ARIA for what HTML can't say, like aria-expanded or aria-describedby wired up with React's useId. And never write an aria-label for a bot. WCAG 2.5.3 Label in Name expects the accessible name to contain the visible label, so voice control users can say what they see.

Myth 5: "We're not in the EU or government, so the law can't reach us"

The European Accessibility Act has applied since 28 June 2025 (EUR-Lex). It covers products and services for EU consumers, including e-commerce, banking, e-books, transport and telephony. Only microenterprises providing services (under 10 staff and €2 million turnover) are exempt, and each member state sets its own penalties. The standard behind it, EN 301 549, published version 4.1.1 with WCAG 2.2 AA in September 2026. At the time of writing it isn't yet cited in the EU's Official Journal, so version 3.2.1 is still the legal reference.

In the US, the ADA Title II rule requires WCAG 2.1 AA for state and local government web content and apps, including what contractors supply. An April 2026 rule moved the dates to 26 April 2027 for entities serving 50,000 people or more and 26 April 2028 for the rest (Federal Register, 2026). The Justice Department says it may revisit the substance, so check before you plan around it.

Enforcement is real but uneven. A month before Carrefour, a Lille court dismissed a similar case against Auchan's online arm on a revenue threshold, and that's under appeal. If you sell into Europe, or a city council buys your software, the law already reaches you.

When: where accessibility enters the lifecycle

  • Design: check contrast and focus styles in your tokens, keep targets at 24 by 24 CSS pixels, and annotate headings and button names in mockups. Developers can't guess what an icon was meant to say.
  • Build: native elements first, a tested library for complex widgets, eslint-plugin-jsx-a11y in the editor.
  • Test: axe-core plus role and keyboard tests on every pull request, then a screen reader pass (NVDA with Chrome, VoiceOver with Safari) on key journeys before each release.
  • Operate: publish an accessibility statement with a working feedback channel, treat accessibility bugs as defects, and re-check production journeys on a schedule.

Where: AWS building blocks and the open-source tools around them

AWS doesn't sell an accessibility service. It gives you pieces that make particular criteria easier, and places to run checks.

  • AWS Amplify UI: accessible React components, including the sign-in Authenticator. Useful on Amplify, unnecessary if you have a design system. Your theme can still break contrast.
  • Amazon Cognito managed login: passkeys and one-time codes from the Essentials tier up, which gives you a sign-in that meets 3.3.8. Test your branding's contrast.
  • Amazon CloudWatch Synthetics: scheduled Playwright canaries (syn-nodejs-playwright-8.0, Playwright 1.61.1) to replay journey tests against production. Its Playwright trails the current release.
  • Amazon Transcribe: live captions, with partial-result stabilization so words don't flicker. Recorded media still needs human-edited captions.
  • Amazon Polly: read-aloud at $4 to $100 per million characters by engine. Word-highlighting speech marks don't work with generative voices.

The core tools are open source. axe-core powers most scanners, Lighthouse included, so Lighthouse isn't a second opinion. pa11y-ci checks a URL list with no test code. And a free screen reader like NVDA is the only tool that tells you what a user will actually hear.

Decide: which check for which problem

Check Catches Misses Cost
eslint-plugin-jsx-a11y Click handlers on divs, missing alt, invalid ARIA Anything that depends on CSS or runtime state Free, seconds
axe-core via Playwright Contrast, names, labels, lang, invalid ARIA, target size with the 2.2 tag Fake controls, focus order, alt text quality Free, seconds per route
Role and keyboard tests Fake controls, broken Tab order, mouse-only journeys Visual issues, meaning Free, you write them
CloudWatch Synthetics canary Regressions in live journeys Whatever the script doesn't test Billed per run
Manual screen reader pass Announcements, reading order, confusing flows Coverage at scale People's time
Testing with disabled users Whether it actually works for them Nothing a cheaper check finds better Recruiting and paying people

When you're not sure which check a requirement needs, walk it down this tree.

flowchart TB
  Q["A WCAG requirement to verify"] --> Q1{"Visible in the DOM alone?<br/>contrast, names, labels, lang"}
  Q1 -->|"yes"| AX["axe-core in CI"]
  Q1 -->|"no"| Q2{"Depends on behavior?<br/>focus, keyboard, announcements"}
  Q2 -->|"yes"| PW["Playwright role<br/>and keyboard test"]
  Q2 -->|"no"| Q3{"Depends on meaning?<br/>alt text, captions, plain language"}
  Q3 -->|"yes"| HR["Human review<br/>AI may draft"]
  Q3 -->|"no"| AU
  AX --> AU["Before any conformance claim<br/>manual audit and user testing"]
  PW --> AU
  HR --> AU

Pick the cheapest check that can see the problem, and never let a passing tool stand in for the audit.

Accessibility also pulls with and against the rest of this series.

NFR Pulls the same way Pulls against it
Security Passkeys and password-manager paste meet 3.3.8 and cut phishing CAPTCHAs, and timeouts without the warning and 20-second extension WCAG 2.2.1 asks for
Compliance The same evidence trail as Part 3 Laws name different versions: 2.1 for ADA Title II, 2.2 in EN 301 549 v4.1.1
Performance Native HTML ships less JavaScript than custom widgets or overlays Throttled announcements arrive after the text does
Availability Announced error messages make degraded modes usable An accessible fallback is one more path to test
Scalability Text and voice share one backend Nova 2 Sonic caps at 20 concurrent sessions per account per Region
Maintainability Fewer custom widgets, less keyboard code Hand-built ARIA widgets need their own tests forever
Cost A lint error takes minutes to fix Manual testing and user research are real budget lines
Sustainability Lean, semantic pages Speech output adds compute per interaction

Hands-on: a WCAG 2.2 AA gate in CI

Take Harbor Works, the fictional plant in Luso, the open-source AI watchman for production lines I'm building alongside this series. Its web app has a status page, a sign-in page and an alerts page, and a shift supervisor has to be able to use each one with a keyboard and a screen reader.

flowchart LR
  PR["Pull request"] --> L["ESLint jsx-a11y<br/>markup rules"]
  L --> B["Build the app"]
  B --> S["Playwright serves<br/>the production build"]
  S --> A["axe-core scan<br/>every route, 2.2 AA tags"]
  A --> J["Role and keyboard<br/>journey test"]
  J -->|"all pass"| M["Merge"]
  L -->|"fail"| F["Build fails with<br/>rule ids and selectors"]
  A -->|"fail"| F
  J -->|"fail"| F
  M --> H["Manual screen reader<br/>pass before release"]

Each stage catches what the one before it can't, and a person still checks key journeys before release.

For the lint step, add jsxA11y.flatConfigs.recommended from eslint-plugin-jsx-a11y 6.10.2 to your eslint.config.js. The Playwright config below assumes a Vite app with npm run preview. Set BASE_URL to test a preview deployment instead.

Checked against Playwright accessibility testing and Playwright reporters on Oct 29, 2026. Ran locally with Node 22, @playwright/test 1.64.1, @axe-core/playwright 4.14.0 and Chromium against static test pages. Not deployed — test in a sandbox account first.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  forbidOnly: !!process.env.CI,
  reporter: [
    ['list'],
    ['html', { open: 'never' }],                              // full report as a CI artifact
    ['junit', { outputFile: 'reports/a11y-junit.xml' }],      // CodeBuild test reports read this
  ],
  use: { baseURL: process.env.BASE_URL ?? 'http://localhost:4173' },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  // Serve the production build unless BASE_URL points at a preview deployment
  webServer: process.env.BASE_URL ? undefined : {
    command: process.env.A11Y_SERVE ?? 'npm run preview -- --port 4173 --strictPort',
    url: 'http://localhost:4173',
    reuseExistingServer: !process.env.CI,
  },
});
Enter fullscreen mode Exit fullscreen mode

The test scans every route with axe, then walks sign-in the way a screen reader user would: by label, role and keyboard.

Checked against Playwright accessibility testing and axe-core API tags on Oct 29, 2026. Ran locally as above. Not deployed — test in a sandbox account first.

// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

// axe-core tags for WCAG 2.0, 2.1 and 2.2 at levels A and AA.
// Drop 'wcag22aa' and the WCAG 2.2 target-size rule never runs.
const WCAG_22_AA = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'];
const ROUTES = (process.env.A11Y_ROUTES ?? '/,/sign-in/,/alerts/').split(',');

for (const route of ROUTES) {
  test(`${route} has no detectable WCAG 2.2 AA violations`, async ({ page }, testInfo) => {
    await page.goto(route);
    await page.getByRole('main').waitFor();            // also fails pages with no main landmark

    const results = await new AxeBuilder({ page })
      .withTags(WCAG_22_AA)
      .exclude('[data-a11y-ignore]')                   // third-party embeds you track as known issues
      .analyze();

    // Full JSON in the report: selectors, failure summaries and help URLs for whoever fixes it
    await testInfo.attach('axe-violations.json', {
      body: JSON.stringify(results.violations, null, 2),
      contentType: 'application/json',
    });

    const summary = results.violations.map(
      (v) => `${v.id} [${v.impact}] x${v.nodes.length}: ${v.help}`,
    );
    expect(summary).toEqual([]);
  });
}

// axe can't tell a clickable <div> from plain text. Role and label queries can:
// if this fails, screen readers, voice control and browser agents can't find the control either.
test('sign-in works by label, role and keyboard alone', async ({ page }) => {
  await page.goto(process.env.A11Y_SIGN_IN ?? '/sign-in/');
  await page.getByLabel('Email').fill('supervisor@harbor.example');
  await page.keyboard.press('Tab');
  const submit = page.getByRole('button', { name: 'Continue' });
  await expect(submit).toBeFocused();                 // reachable, in order, by keyboard
  await page.keyboard.press('Enter');
  await expect(page.getByRole('heading', { level: 1, name: 'Alerts' })).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Running it showed three things. A broken test page failed with four named rules (html-has-lang, image-alt, link-name, color-contrast). The clickable-div page passed the scan and failed the journey test. And axe-core 4.14 only checks target size when you ask for the wcag22aa tag. A default run let 10-pixel buttons through. Keep exclude rare, since it switches off every rule inside that element.

On GitHub Actions:

Checked against Playwright CI docs on Oct 29, 2026. YAML validated, not run on GitHub. Not deployed — test in a sandbox account first.

# .github/workflows/a11y.yml
name: a11y
on:
  pull_request:
  push:
    branches: [main]

jobs:
  a11y:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx eslint .                       # jsx-a11y: div buttons, missing alt, before a browser starts
      - run: npm run build
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test                # GitHub sets CI=true, so test.only fails the build
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}                 # upload the report when tests fail, too
        with:
          name: a11y-report
          path: playwright-report/
          retention-days: 30
Enter fullscreen mode Exit fullscreen mode

If your pipeline runs in AWS, here's the same gate for CodeBuild. Playwright supports only Debian and Ubuntu, so use the Ubuntu standard image. CodeBuild doesn't document setting CI, so the buildspec sets it. The JUnit file feeds CodeBuild test reports, so each failing page shows up as a failed test case.

Checked against the CodeBuild buildspec reference and CodeBuild available runtimes on Oct 29, 2026. YAML validated, not run in CodeBuild. Not deployed — test in a sandbox account first.

# buildspec.yml
# CodeBuild image: aws/codebuild/standard:7.0 (Ubuntu 22.04). Playwright supports Debian and Ubuntu only.
# Pinned alternative image: mcr.microsoft.com/playwright:v1.64.1-noble
version: 0.2

env:
  variables:
    CI: "true"                 # the Playwright config reads it

phases:
  install:
    runtime-versions:
      nodejs: 22
    commands:
      - npm ci
      - npx playwright install --with-deps chromium
  pre_build:
    commands:
      - npx eslint .
      - npm run build
  build:
    commands:
      - npx playwright test

reports:
  a11y-tests:                  # becomes report group <project-name>-a11y-tests
    files:
      - a11y-junit.xml
    base-directory: reports
    file-format: JUNITXML

artifacts:
  name: a11y-report
  base-directory: playwright-report
  files:
    - '**/*'
Enter fullscreen mode Exit fullscreen mode

That's the automated slice, about half the problem by the most generous count. The manual pass in the diagram isn't optional.

Accessibility in the age of AI

Myth 6: "AI makes accessibility free"

The Justice Department answered this one. Delaying the ADA dates, it wrote that "advanced technology, such as generative AI, does not yet reliably automate the remediation of inaccessible content at scale". That's a regulator that wanted the shortcut to exist.

Research agrees. A 2025 study comparing web code from GPT-4o and Qwen2.5-Coder with human code found the models often better on basics like contrast and alt text, and weak on ARIA. Better prompts helped a little. Feeding test results back to the model helped a lot. WebAIM even suggests AI-assisted "vibe coding" may be part of why errors rose in 2026, though it didn't measure that.

Alt text shows why "free" is the wrong word. A model sees pixels, but alt text depends on purpose: the same photo needs empty alt as decoration and a description when it informs. Screen reader users can already get generic descriptions themselves (60.1% in WebAIM's 2026 screen reader survey use AI to describe images). What only you can add is why the image is there. Let AI draft alt text and captions, and have a person approve them.

AI as a workload: streaming answers and live regions

Streaming makes chat feel fast and can make it unbearable with a screen reader. If the answer container is an aria-live region, every token is a change, and users hear fragments, restarts, or a queue that runs long after the text has finished.

The fix is to separate what's shown from what's announced. A Harbor Works supervisor asks why line 3 stopped. The visible answer updates per token. A separate role="status" region, polite by default, only gets whole sentences, at most one every 1.5 seconds, then "Answer complete."

sequenceDiagram
  participant U as Supervisor
  participant UI as Chat UI
  participant B as Bedrock
  participant L as Live region
  participant SR as Screen reader
  U->>UI: Why did line 3 stop
  UI->>B: ConverseStream request
  loop every token
    B-->>UI: contentBlockDelta text
    UI->>UI: Append to visible answer and sentence buffer
    opt sentence ended and 1.5 s since last announcement
      UI->>L: Replace text with the whole sentence
      L->>SR: Polite announcement
    end
  end
  B-->>UI: messageStop
  UI->>L: Remaining text plus Answer complete
  L->>SR: Final polite announcement

Tokens reach the screen as fast as they arrive, but the screen reader only hears whole sentences.

Your backend calls Bedrock's ConverseStream and forwards each contentBlockDelta text, and getTokens reads that stream. Add a standard visually-hidden CSS class.

Checked against MDN ARIA live regions and the Bedrock ConverseStream reference on Oct 29, 2026. Type-checked with React 19 and run in Chromium against a simulated stream, not with a real screen reader or Bedrock. Not deployed — test in a sandbox account first.

import { useEffect, useRef, useState } from 'react';

type Props = {
  // A factory, not a stream: StrictMode runs effects twice, so each run needs its own request
  getTokens: (signal: AbortSignal) => AsyncIterable<string>;
  announce?: 'sentences' | 'complete'; // let users pick, and save it in their settings
};

const MIN_GAP_MS = 1500;               // never speak more often than this
const SENTENCE_END = /[.!?]\s*$|\n$/;

export function StreamingAnswer({ getTokens, announce = 'sentences' }: Props) {
  const [shown, setShown] = useState('');    // what sighted users see: every token
  const [spoken, setSpoken] = useState('');  // what the live region says: whole sentences
  const pending = useRef('');
  const lastSpoke = useRef(0);

  useEffect(() => {
    const ctrl = new AbortController();
    setShown(''); setSpoken(''); pending.current = '';
    (async () => {
      try {
        for await (const t of getTokens(ctrl.signal)) {
          if (ctrl.signal.aborted) return;
          setShown((s) => s + t);
          pending.current += t;
          const now = Date.now();
          if (announce === 'sentences' && SENTENCE_END.test(pending.current)
              && now - lastSpoke.current >= MIN_GAP_MS) {
            setSpoken(pending.current.trim());
            pending.current = '';
            lastSpoke.current = now;
          }
        }
        if (ctrl.signal.aborted) return;
        // Flush the tail, then say it's finished so nobody sits waiting for more
        setSpoken(announce === 'complete'
          ? 'Answer ready.'
          : `${pending.current.trim()} Answer complete.`);
        pending.current = '';
      } catch {
        if (!ctrl.signal.aborted) setSpoken('The answer failed to load. Try again.');
      }
    })();
    return () => ctrl.abort();
  }, [getTokens, announce]);

  return (
    <section aria-labelledby="answer-heading">
      <h2 id="answer-heading">Answer</h2>
      <div>{shown}</div>{/* not live: reading it is the user's choice */}
      {/* In the DOM from first render so screen readers register it before it changes */}
      <div role="status" className="visually-hidden">{spoken}</div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

In a simulated run, a 48-token answer produced two announcements instead of 48 and passed the same axe scan. Some users would rather read the finished answer at their own pace, so make announce="complete" a user setting.

Voice interfaces

Voice helps users with motor or vision impairments, and anyone whose hands are busy. On AWS you can chain Amazon Transcribe, a text model and Amazon Polly, keeping a transcript at every step. Or use Amazon Nova 2 Sonic (amazon.nova-2-sonic-v1:0, generally available since December 2025), a speech-to-speech model on Bedrock's InvokeModelWithBidirectionalStream API that accepts speech and text in one session, in seven languages.

Know its limits. It runs In-Region only (us-east-1, us-west-2, eu-north-1, ap-northeast-1), connections last eight minutes, and the quota of 20 concurrent sessions per account per Region isn't adjustable. Its model card lists Bedrock Guardrails as unsupported, so content controls live in your prompts and tools. And voice must never be the only way in. A Deaf user needs the same task in text.

AI agents as users

Browser agents read the accessibility tree, so Myths 1 and 4 apply to them as well. A form a screen reader can finish is a form an agent can finish, which makes your getByRole tests a fair proxy for agent-readiness. If an agent can't find a button, the fix is the same native <button> a person needs.

AI as an engineering aid

Assistants happily generate a div with an onClick, and that 2025 study suggests prompting alone won't fix it. Feedback does. So:

  • Put "native elements, a label on every input, no click handlers on non-interactive elements" in your assistant's project instructions.
  • Feed it the axe JSON and the failing role test, not "make it accessible".
  • Have a person review every ARIA attribute it adds, since that's where models were weakest.

What's still immature is AI doing the manual pass. No tool today replaces a person with a screen reader on your checkout.

Who: owners and the shared responsibility split

Activity Product owner Designer Developers QA / platform
Set the target and legal scope, with legal advice A C C C
Contrast, focus and target-size tokens I R/A C I
Semantic components, labels, live regions I C R/A C
CI gate and production canaries I I R A
Screen reader pass before release C C R A
Approve AI-drafted alt text and captions A R R I

R = responsible, A = accountable, C = consulted, I = informed.

Shared responsibility barely changes with compute type. On EC2, containers or Lambda, AWS owns its console's accessibility and nothing in your app. With Amplify UI, AWS owns component semantics, and you own labels, composition and theme. With Bedrock, the provider owns what the model generates, and you own how it's presented, announced, captioned and reviewed.

Monday checklist

  • [ ] Write down your target (WCAG 2.2 AA) and which laws reach you: the EAA, ADA Title II through a public-sector customer, or both.
  • [ ] Add eslint-plugin-jsx-a11y with the recommended config and fix what it finds in shared components.
  • [ ] Add the axe-core Playwright test with the wcag22aa tag to CI for your three busiest routes.
  • [ ] Write one role-and-keyboard journey test for sign-in and one for your main task.
  • [ ] Check design tokens for 4.5:1 text contrast and a visible focus style.
  • [ ] Spend 15 minutes on checkout or sign-in with NVDA or VoiceOver, and file what you hear.
  • [ ] If you stream LLM output, announce sentences from a separate role="status" region.
  • [ ] Put a human approval step on AI-generated alt text and captions.

Conclusion

Every myth here grows from one belief: that accessibility is a feature you add, buy or generate. The evidence says it's mostly a property of ordinary code. Six boring error types cover nearly every failure. A real <button> beats a clever widget. Scanners find part of the problem, overlays and AI drafts don't close the rest, and courts and regulators have stopped accepting "mostly".

Carrefour's 71% measured the wrong thing. Your users don't experience a percentage. They finish the task or they don't.

So build the gate, write the role tests, and put a person with a screen reader on the journeys that matter.

If a screen reader can't find it, neither can your users, your tests or your agents.


Series: Non-functional Application Requirements

  1. Non-functional Application Requirements: An Introduction
  2. Non-functional Application Requirements: Security
  3. Non-functional Application Requirements: Compliance
  4. Accessibility
  5. Performance (coming soon)
  6. Availability (coming soon)
  7. Scalability (coming soon)
  8. Maintainability (coming soon)
  9. Cost (coming soon)
  10. Sustainability (coming soon)

Cover image: AWS re:Invent 2023 – Keynote with Dr. Werner Vogels

Next up, Performance: p95 budgets, caching layers and time-to-first-token, and why a page that's merely less slow still sells more.

Sources

Top comments (0)