Skip to content

Accessibility Development Workflow

End-to-End Accessibility

Accessibility is not a testing phase. It flows through every stage of the development pipeline, from design to deployment. This page maps GE's accessibility controls at each stage.


Overview

Design → Spec → TDD → Implementation → Testing → Review → Deployment
  ↑        ↑      ↑         ↑              ↑        ↑         ↑
Alexander  Anna  Antje   Semantic HTML   axe-core  Marta/   Lighthouse
Accessible  a11y  a11y   ARIA when      Screen    Iwona     CI
Components  reqs  cases  needed         reader    a11y      threshold
                                        manual    gate

Every stage has accessibility deliverables. Skipping a stage creates debt that compounds downstream.


Stage 1: Design

Owner: Alexander Deliverable: Accessible component specifications

Accessible Design System

Alexander maintains a design system where every component is accessible by default:

  • Color palette with WCAG 2.2 AA contrast ratios verified
  • Typography scale with minimum readable sizes
  • Component states: default, hover, focus, active, disabled, error
  • Focus indicators: visible, 2px minimum, 3:1 contrast ratio
  • Touch targets: minimum 24x24 CSS pixels (44x44 recommended)
  • Motion: respects prefers-reduced-motion

Color Accessibility

Context Minimum Contrast Example
Normal text (< 18pt) 4.5:1 Body copy, labels, links
Large text (>= 18pt or 14pt bold) 3:1 Headings, large buttons
UI components and graphics 3:1 Borders, icons, form controls
Focus indicators 3:1 Focus rings, outlines

Critical rule: Never use color as the only means of conveying information. Always pair color with text, icons, or patterns.

Bad:  Red = error, green = success (color only)
Good: Red + "Error: ..." text + icon, Green + "Success: ..." text + icon

Component Design Checklist

For every new component, Alexander specifies:

  • [ ] Keyboard interaction pattern (which keys do what)
  • [ ] Focus management (where focus goes on open/close/submit)
  • [ ] Screen reader announcement (what is read, in what order)
  • [ ] Touch target size (minimum 24x24, recommended 44x44)
  • [ ] Color contrast for all states
  • [ ] Behavior with prefers-reduced-motion
  • [ ] Behavior with prefers-color-scheme
  • [ ] Text resize behavior up to 200%

Stage 2: Specification

Owner: Anna Deliverable: Accessibility requirements in feature spec

Accessibility Requirements

Anna includes accessibility requirements in every feature specification. These are not separate from functional requirements — they are part of them.

Example specification section:

## Accessibility Requirements

### Keyboard Navigation
- Tab order follows visual layout (left-to-right, top-to-bottom)
- Modal dialog traps focus within the dialog
- Escape key closes the modal and returns focus to trigger
- Arrow keys navigate within dropdown options

### Screen Reader
- Form fields have visible labels associated via `for`/`id`
- Error messages are associated with fields via `aria-describedby`
- Dynamic content updates use `aria-live="polite"`
- Modal announces its title when opened

### Visual
- All text meets 4.5:1 contrast ratio
- Focus indicator visible on all interactive elements
- Error states use icon + text, not color alone

User Stories Include Accessibility

Accessibility is not a separate user story. It is part of every user story.

Bad:  "As a user, I can submit the contact form."
      (Separate story: "As a screen reader user, I can submit the form.")

Good: "As a user, I can submit the contact form using mouse, keyboard,
       or assistive technology, with all fields labeled and errors
       announced."

Stage 3: TDD — Test-Driven Development

Owner: Antje Deliverable: Accessibility test cases written before implementation

a11y Test Cases

Antje writes test cases that verify accessibility before the implementing agent writes a single line of component code.

Categories of accessibility tests:

Structural Tests

test('form fields have associated labels', () => {
  render(<ContactForm />);
  const emailInput = screen.getByLabelText('Email address');
  expect(emailInput).toBeInTheDocument();
  expect(emailInput).toHaveAttribute('type', 'email');
  expect(emailInput).toHaveAttribute('aria-required', 'true');
});

test('heading hierarchy is correct', () => {
  render(<Page />);
  const headings = screen.getAllByRole('heading');
  // h1 appears exactly once
  const h1s = headings.filter(h => h.tagName === 'H1');
  expect(h1s).toHaveLength(1);
});

Keyboard Navigation Tests

test('modal traps focus', () => {
  render(<ModalDialog />);
  const trigger = screen.getByRole('button', { name: 'Open dialog' });

  fireEvent.click(trigger);

  // Focus moves to dialog
  const dialog = screen.getByRole('dialog');
  expect(dialog).toHaveFocus();

  // Tab stays within dialog
  userEvent.tab();
  expect(within(dialog).getByRole('button', { name: 'Cancel' })).toHaveFocus();

  // Escape closes and returns focus
  userEvent.keyboard('{Escape}');
  expect(dialog).not.toBeInTheDocument();
  expect(trigger).toHaveFocus();
});

test('dropdown navigable with arrow keys', () => {
  render(<Dropdown options={['Option 1', 'Option 2', 'Option 3']} />);
  const trigger = screen.getByRole('combobox');

  fireEvent.keyDown(trigger, { key: 'ArrowDown' });
  expect(screen.getByRole('option', { name: 'Option 1' })).toHaveFocus();

  fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
  expect(screen.getByRole('option', { name: 'Option 2' })).toHaveFocus();
});

Screen Reader Announcement Tests

test('error message announced to screen readers', () => {
  render(<ContactForm />);
  const submitButton = screen.getByRole('button', { name: 'Submit' });

  fireEvent.click(submitButton);

  const errorMessage = screen.getByRole('alert');
  expect(errorMessage).toHaveTextContent('Please fill in required fields');
});

test('live region announces status updates', () => {
  render(<SearchResults />);
  const liveRegion = screen.getByRole('status');

  // After search completes
  expect(liveRegion).toHaveTextContent('5 results found');
});

Automated a11y Tests

import { axe, toHaveNoViolations } from 'jest-axe';

test('page has no accessibility violations', async () => {
  const { container } = render(<Page />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Stage 4: Implementation

Owner: Floris (architecture), Floor (components) Deliverable: Accessible, semantic HTML with ARIA when needed

Semantic HTML First

The first rule of ARIA: do not use ARIA if native HTML can do it.

<!-- Bad: ARIA role on a div -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>

<!-- Good: native button element -->
<button type="submit">Submit</button>
<!-- Bad: div soup with ARIA landmarks -->
<div role="navigation">
  <div role="list">
    <div role="listitem"><a href="/">Home</a></div>
  </div>
</div>

<!-- Good: semantic HTML -->
<nav>
  <ul>
    <li><a href="/">Home</a></li>
  </ul>
</nav>

Semantic Element Reference

Purpose Use Not
Page regions <header>, <nav>, <main>, <footer>, <aside> <div> with ARIA roles
Headings <h1> through <h6> in order <div class="heading">
Lists <ul>, <ol>, <li> <div> with bullet styling
Tables <table>, <th>, <td> with scope <div> grid with CSS
Forms <form>, <fieldset>, <legend>, <label> <div> with click handlers
Buttons <button> <div onclick>, <a href="#">
Links <a href="..."> <span onclick>
Emphasis <strong>, <em> <span class="bold">

When to Use ARIA

ARIA is appropriate when:

  • Custom widgets have no native HTML equivalent (tabs, tree views, comboboxes)
  • Dynamic content updates need to be announced (aria-live)
  • Relationships between elements need explicit declaration (aria-describedby)
  • States need to be communicated (aria-expanded, aria-selected, aria-checked)
  • Error messages need association with inputs (aria-errormessage, aria-invalid)

ARIA Patterns for Common Widgets

Tabs:

<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    Profile
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
    Security
  </button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <!-- Profile content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <!-- Security content -->
</div>

Disclosure (expand/collapse):

<button aria-expanded="false" aria-controls="details-content">
  Show details
</button>
<div id="details-content" hidden>
  <!-- Expandable content -->
</div>

Alert/Status:

<!-- For urgent, important messages -->
<div role="alert">
  Payment failed. Please check your card details.
</div>

<!-- For non-urgent status updates -->
<div role="status" aria-live="polite">
  3 items added to cart.
</div>

Focus Management

Focus management is critical in single-page applications where content changes without a page reload.

Rules:

  1. When a modal opens, focus moves to the modal (first focusable element or the dialog itself)
  2. When a modal closes, focus returns to the element that opened it
  3. When content is deleted, focus moves to a logical next element
  4. When a page section loads dynamically, focus stays where it was (use aria-live to announce changes)
  5. Never move focus unexpectedly
  6. All focusable elements have a visible focus indicator

Forms

<form>
  <div>
    <label for="email">Email address</label>
    <input
      type="email"
      id="email"
      name="email"
      aria-required="true"
      aria-describedby="email-help email-error"
      aria-invalid="true"
    />
    <p id="email-help">We will only use this to send your receipt.</p>
    <p id="email-error" role="alert">Please enter a valid email address.</p>
  </div>

  <fieldset>
    <legend>Notification preferences</legend>
    <label>
      <input type="checkbox" name="notify-email" />
      Email notifications
    </label>
    <label>
      <input type="checkbox" name="notify-sms" />
      SMS notifications
    </label>
  </fieldset>

  <button type="submit">Submit order</button>
</form>

Form rules:

  • Every input has a visible <label> associated via for/id
  • Never use placeholder as a label substitute
  • Group related inputs with <fieldset> and <legend>
  • Associate error messages with inputs via aria-describedby
  • Mark invalid fields with aria-invalid="true"
  • Describe errors in text, not just color

Images

<!-- Informative image: alt text describes content -->
<img src="chart.png" alt="Revenue grew 40% from Q1 to Q4 2025" />

<!-- Decorative image: empty alt -->
<img src="divider.png" alt="" />

<!-- Complex image: detailed description -->
<figure>
  <img src="architecture.png" alt="System architecture diagram"
       aria-describedby="arch-desc" />
  <figcaption id="arch-desc">
    Three-layer architecture: client browser connects to API gateway
    via TLS, which routes to internal services via mTLS. Services
    access PostgreSQL database with role-based credentials.
  </figcaption>
</figure>

<!-- Icon with text: icon is decorative -->
<button>
  <svg aria-hidden="true"><!-- icon SVG --></svg>
  Delete item
</button>

<!-- Icon-only button: needs accessible name -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- X icon SVG --></svg>
</button>

Stage 5: Testing

Owner: Antje (test cases), implementing agent (execution) Deliverable: Accessibility verified through automated + manual testing

Automated Testing

Automated tools catch approximately 30-40% of accessibility issues. They are necessary but not sufficient.

Tools in the GE pipeline:

Tool What It Catches Integration Point
axe-core WCAG violations in rendered DOM Unit tests (jest-axe)
Lighthouse Page-level accessibility score CI/CD pipeline
eslint-plugin-jsx-a11y Accessibility issues in JSX at write time Linting
Pa11y Page-level WCAG conformance CI/CD pipeline

axe-core in unit tests:

import { axe } from 'jest-axe';

test('component has no a11y violations', async () => {
  const { container } = render(<Component />);
  expect(await axe(container)).toHaveNoViolations();
});

Lighthouse CI threshold:

lighthouse:
  accessibility:
    score: 95    # Minimum score to pass
    # 100 is the target, 95 allows for edge cases
    # that require manual verification

Manual Testing

Automated tools miss 60-70% of accessibility issues. Manual testing is required.

Screen reader testing matrix:

Screen Reader Browser OS Priority
NVDA Firefox Windows Primary
VoiceOver Safari macOS/iOS Primary
TalkBack Chrome Android Secondary
JAWS Chrome Windows Reference

Manual testing checklist:

  • [ ] Keyboard only: Complete all tasks without a mouse
  • [ ] Tab order: Logical and follows visual layout
  • [ ] Focus visibility: Every focused element has a visible indicator
  • [ ] Screen reader: Content is announced in logical order
  • [ ] Screen reader: Interactive elements announce role, name, and state
  • [ ] Screen reader: Dynamic content changes are announced
  • [ ] Zoom 200%: All content remains visible and functional
  • [ ] High contrast mode: Content visible in Windows High Contrast Mode
  • [ ] Reduced motion: Animations respect prefers-reduced-motion
  • [ ] No mouse: All hover-only information is available via focus or other means

Stage 6: Code Review

Owner: Marta, Iwona Deliverable: Accessibility verified in merge gate

Accessibility Review Checklist

Marta and Iwona check accessibility as part of every code review:

  • [ ] Semantic HTML used (no <div> as button, no <span> as link)
  • [ ] ARIA used correctly and only when necessary
  • [ ] Images have appropriate alt text
  • [ ] Forms have labels, fieldsets, and error handling
  • [ ] New components have keyboard interaction
  • [ ] Focus management handles dynamic content
  • [ ] Color is not the only means of conveying information
  • [ ] axe-core tests included for new components
  • [ ] No inline styles that break user stylesheets

Merge blocked if:

  • axe-core tests fail
  • Lighthouse accessibility score below threshold
  • New component lacks keyboard support
  • Form fields lack labels

Stage 7: Deployment

Owner: CI/CD pipeline Deliverable: Accessibility gates pass in production-like environment

Lighthouse CI

Lighthouse accessibility audit runs in the deployment pipeline against the staging environment.

Gate configuration:

assertions:
  accessibility:
    - minScore: 0.95
      level: error    # Blocks deployment
  categories:accessibility:
    - minScore: 0.90
      level: warn     # Warns but allows deployment

Post-Deployment Monitoring

  • Lighthouse scheduled scans weekly on production
  • Regression alerts if score drops below threshold
  • Accessibility audit as part of quarterly review

Component-Level Accessibility Checklist

This checklist flows through the pipeline. Each stage adds verification.

Check Design Spec TDD Impl Test Review
Contrast ratios x x
Touch targets x x x
Keyboard interaction x x x x x x
Focus management x x x x x x
Screen reader labels x x x x x
Semantic HTML x x x
ARIA correctness x x x
Error handling x x x x x
Motion/animation x x x
Responsive/zoom x x x
axe-core clean x x x

Further Reading