Playbooks
The Accessibility Compliance Baseline Playbook
By Flora May dela Cruz
The four global primitives every app owes the user, the WCAG row-by-row enforcement matrix, and the merge checklist that catches regressions before they ship. Companion to the Accessibility Annotation Playbook.
Purpose
The Accessibility Annotation Playbook is about how to spec accessible behavior into a design. This is its companion: how to prove it shipped. WCAG 2.1 Level AA is the procurement bar for almost every enterprise customer, and the way teams pass it is not heroics on the last sprint — it’s four global primitives wired into the app shell once, a row-by-row matrix that names where each success criterion is enforced, and a small PR checklist that prevents regressions.
This playbook is the baseline. It’s deliberately not a complete a11y guide. It’s the smallest set of moves that, applied once, make 80% of WCAG AA pass by default for every future feature.
When to use it
- A new product where accessibility is a stated requirement (it almost always is for enterprise)
- A product being audited for the first time and you need to know what to fix first
- A prototype being prepared for user research that will include AT users
- A team adding their first accessibility-trained engineer or designer who needs a map
- After every feature merge, as a regression check
Skip this for one-off marketing pages — those have their own compliance pattern (mostly contrast, alt text, and reduced-motion). This playbook is for application surfaces with navigation, dialogs, live updates, and data grids.
Core framework
The four global primitives
Most accessibility work scales linearly with the number of features. Four primitives don’t — wire them in once at the shell level and every future feature inherits them.
1. Skip-to-content link. First tabbable element on every page. Visually hidden until focused. Target: #main-content. Without it, keyboard users tab through the entire nav on every page load.
2. Programmatically focusable main region. A single <main id="main-content" tabIndex={-1}> per route. The skip link’s target, and the landing spot for route-transition focus moves when individual features need them.
3. A global polite live region. One <div id="app-live-region" role="status" aria-live="polite" aria-atomic="true"> mounted at the shell. Visually hidden. Any feature that needs to announce a result (toast appeared, refresh complete, copy succeeded, route changed) pushes text into it via a single imperative helper. No per-feature live regions; one is enough.
4. A document.title effect tied to routing. When the route changes, the document title updates from a small map of route → title. Screen readers announce title changes on most platforms; this is the cheapest, most universal route-change announcement available.
Build these four once. Stop solving them per-feature.
The compliance matrix
For each WCAG 2.1 success criterion the team commits to, name where it is enforced — the shell, a shared component, or a per-feature requirement. The matrix doubles as both a checklist and an architecture map.
A working matrix has three columns: the criterion, the title, the location of enforcement. Examples (this is the structure, not the full list):
| SC | Title | Where enforced |
|---|---|---|
| 1.1.1 | Non-text Content | Per-feature: decorative SVGs marked aria-hidden; meaningful icons get aria-label on their parent control |
| 1.3.1 | Info and Relationships | Shell: one main landmark, header/nav/aside landmarks. Per-feature: heading hierarchy starts with the page h1 |
| 1.4.1 | Use of Color | Per-feature: status pills always include text alongside color; charts always paired with adjacent numeric label |
| 1.4.3 | Contrast (Minimum) | Design tokens: all text colors selected against AA contrast at default backgrounds |
| 1.4.10 | Reflow | Layout system (see Enterprise Reflow Playbook); data tables exempted via WCAG table exception |
| 1.4.11 | Non-text Contrast | Design tokens: focus ring token meets 3:1 against all surfaces |
| 2.1.1 | Keyboard | Per-feature: real <button> over <div onClick>; if div is required, role=button + tabIndex + onKeyDown |
| 2.1.2 | No Keyboard Trap | Shell: Escape closes mobile nav overlay; component library: dialogs handle Escape natively |
| 2.3.3 | Animation from Interactions | Global CSS rule: prefers-reduced-motion: reduce disables transitions and animations |
| 2.4.1 | Bypass Blocks | Shell: skip-to-content link |
| 2.4.2 | Page Titled | Shell: document.title effect |
| 2.4.3 | Focus Order | Shell: focus returns to trigger when overlay closes |
| 2.4.7 | Focus Visible | Design tokens: all interactives expose :focus-visible outlines |
| 3.1.1 | Language of Page | HTML lang attribute set once at the document root |
| 4.1.2 | Name, Role, Value | Per-feature: icon-only buttons get aria-label; disclosure buttons get aria-expanded; toggle buttons get aria-pressed |
| 4.1.3 | Status Messages | Shell: global polite live region + one imperative helper |
The pattern: ten or so rows enforced once at the shell or via tokens, the rest enforced per feature. The shell rows are non-negotiable infrastructure. The per-feature rows are the merge-checklist material.
The acceptance checklist
Eight lines, applied to every PR adding or modifying a user-facing feature.
- No new
<div onClick>withoutrole="button",tabIndex={0}, andonKeyDownhandling Enter and Space - Every new icon-only button has an
aria-labelAND a tooltip - Every new image has
alt(empty if decorative) - Every new color- or state-encoding has a non-color companion: text, icon, or pattern
- Every new dialog or drawer either uses the component-library dialog primitive (which handles focus trap and Escape) OR carries
role="dialog",aria-modal, Escape handling, and focus restore on close - Every new animation is gated by
prefers-reduced-motionor limited to ≤ 200ms - Every new route has a matching entry in the document-title map
- The merged route scores ≥ 95 on an automated a11y audit
Eight items, ten minutes per PR. The matrix tells the team what compliance means; the checklist is how they enforce it without a specialist on every review.
Reusable template
# Accessibility baseline — <product>
## Target
- WCAG 2.1 Level AA
## Global primitives (built once at the shell)
- [ ] Skip-to-content link, first tabbable, hidden until focused
- [ ] <main id="main-content" tabIndex={-1}> on every route
- [ ] <div id="app-live-region" role="status" aria-live="polite" aria-atomic="true">
- [ ] document.title effect mapping route → human-readable title
## Compliance matrix
<paste matrix; one row per committed SC; column for where enforced>
## Acceptance checklist (per PR)
<paste 8-line checklist>
## Testing
- Automated: lint plugin for jsx-a11y, runtime axe in dev, Lighthouse on preview deploys
- Manual: keyboard-only sweep, screen reader on macOS + Windows, reduced-motion enabled,
400% zoom, forced-colors / high-contrast
## Known gaps (with named owners and target dates)
| Item | Owner | Target |
|---|---|---|
| ... | ... | ... |
The Known Gaps table is what separates a real baseline from a wishlist. Every gap has an owner. Every gap has a date. A baseline document with no gaps named is either lying or hasn’t been audited yet.
AI-assisted workflow
Prompt: derive the compliance matrix from a feature list
Below is a list of features in an enterprise app. For each WCAG 2.1
Level AA success criterion, name where it should be enforced for this
product. Use these three locations:
1. SHELL — built once at the application root
2. DESIGN TOKENS / COMPONENT LIBRARY — enforced by shared primitives
3. PER-FEATURE — must be specced and reviewed feature-by-feature
Output: a three-column table (SC number, title, location). If the
location depends on the feature, name the dependency.
<paste feature list>
Prompt: audit a component spec for compliance gaps
Below is a component spec. Identify every WCAG 2.1 Level AA gap and
classify each as one of:
- "Missing semantic": role, aria-label, or landmark not specified
- "Missing keyboard": no keyboard equivalent for a mouse action
- "Missing state": dynamic state not exposed (aria-expanded,
aria-pressed, aria-sort, etc.)
- "Color-only encoding": status conveyed only by color
- "Animation harm": animation not gated by prefers-reduced-motion or
exceeding 200ms without justification
For each gap, suggest the minimal fix.
<paste spec>
Both prompts are checkpoint tools, not generators. The matrix and the spec still need a human author. AI tightens them.
Collaboration considerations
- For engineers: the four global primitives are one engineering ticket, not eight. Scope it as one. The payoff compounds across every future feature.
- For PMs: WCAG AA is procurement-required for nearly every enterprise customer. The matrix is the most efficient artifact to hand to a customer’s accessibility team during sales — it answers “how do you ensure WCAG AA” without anyone having to write a custom response.
- For designers: the annotation playbook describes what to spec; this describes what survives once it’s built. Read them together. The 4.1.3 status-message row in particular is something designers often spec inconsistently per feature; pointing at the one live region in the shell makes it consistent.
- For QA: the eight-item PR checklist is more catch-rate than any one tool. Automate what you can (jsx-a11y, axe-core, Lighthouse) and treat the checklist as the thing humans verify.
- For accessibility specialists / consultants: the matrix is the artifact to negotiate. Disagreement about “where is this enforced” is what teams should have explicit conversations about; the matrix turns those conversations from generic into concrete.
Common failure patterns
- One live region per feature. Each toast component re-implements its own
aria-livediv. Screen readers announce stacked or competing messages. Use one. - No skip link. Easy to forget; one of the highest-leverage 30 minutes of work in any application.
<main>not focusable. Skip link works on the first activation, then subsequent activations do nothing because focus has nowhere to land.- Document title never updates. All routes are titled “App” in the browser tab. Both confusing and a WCAG 2.4.2 fail.
- Per-feature focus-ring re-implementation. Inconsistent focus rings across components, often with contrast failures. Put the ring in tokens.
prefers-reduced-motiononly respected in CSS, not in JS. A Houdini paint worklet, a canvas animation, or a JS-driven gauge keeps moving. Gate registration of the animation itself.- Icon-only button with
aria-labelbut no tooltip. Sighted keyboard users have no idea what the button does. - Color-only status pill. Green badge, red badge, no text. Fail for color-blind users and anyone using a high-contrast theme that may render both as the same shade.
- Hand-rolled dialog without focus trap. Tab leaks out to the background page. Use the component-library dialog whenever possible.
- No Known Gaps table. Document claims full compliance and the auditor finds three gaps in fifteen minutes. Naming gaps openly is faster than discovering them late.
Generalized example
A fictional fleet-management dashboard. Application shell wired with: a skip link, a focusable <main>, a single polite live region, and a route-to-title map. The compliance matrix is reviewed at the start of the project; ten SCs are enforced once (shell + tokens), six are flagged per-feature. The PR checklist is added to the team’s pull-request template.
Six months in, a new feature adds a “schedule recall” action. The PR checklist catches three issues the author hadn’t thought about: the icon-only “more actions” menu has an aria-label but no tooltip, the success toast was about to spin up its own aria-live div instead of calling announce(), and the modal’s Escape handler was missing because it was hand-rolled instead of using the dialog primitive. Fixed in fifteen minutes — instead of caught by an auditor four sprints later.
That’s the whole point of the baseline. Compliance scales when most of it is invisible infrastructure and the rest is an eight-line checklist.
Public-safe review (verified before publish)
- No employer or client product names, codenames, or org names
- No customer names, segment sizes, or identifiable details
- No internal metrics, thresholds, OKRs, or telemetry numbers
- No roadmap, ship dates, or future plans
- No architecture, service names, API shapes, or schema fields from real systems
- No screenshots showing real chrome, real data, or recognizable surfaces
- No internal-only workflows, tools, or terminology
- Every example is fictional or abstracted; numbers are illustrative
- A peer outside any employer could read this and learn nothing proprietary
Take this playbook with you
Drop your email to copy the markdown or download the file. One email unlocks every playbook in the Toybox.
No spam. Occasional notes on new playbooks. Unsubscribe in one click.