Skip to main content

Playbooks

Accessibility Annotation Playbook

By Flora May dela Cruz

What to annotate, when, and how — so designs ship with accessibility built-in, not bolted on at the end.

accessibilitya11yannotationshandoffwcag

Purpose

Most accessibility failures don’t come from designers not caring. They come from a designer’s intent never reaching the developer’s keyboard. The screen reader text never gets specified. The focus order never gets drawn. The “what is the empty state when the user has no permission” question never gets asked until the bug is in production.

This playbook is a contract: a fixed shape for the annotations a designer adds to a spec so the engineer can implement an accessible component without guessing, and so accessibility review has something concrete to check.

When to use it

  • Any feature that ships an interactive component (custom select, dialog, drawer, tabs, tree, data grid, anything composite)
  • Any feature with state — loading, empty, error, partial, stale, permission-denied — because those states are where a11y most commonly breaks
  • Any feature with media — images, video, audio, charts, icons that carry meaning
  • Any feature that introduces new motion, color, or contrast decisions

Skip the formal annotation pass when the feature is purely composing already-annotated primitives from your design system without altering their behavior. (But still confirm focus order and announce text for the new arrangement.)

Core framework

Annotate in five layers, in this order. Each layer answers one question that an engineer or AT user will otherwise have to guess.

1. Semantics          → What is this thing?
2. Name and role      → How does AT identify it?
3. Focus and keyboard → How does someone navigate it without a mouse?
4. State and updates  → What changes, and how is the change announced?
5. Reading and reach  → What is the experience without sight, sound, or steady hands?

Each layer has a small, fixed set of properties. If a property doesn’t apply, write n/adon’t leave it blank, because a blank reads as “not yet considered.”

Layer-by-layer

1. Semantics. Which HTML or platform primitive owns this. A button is a button. A link goes somewhere; a button does something. A region with a heading is a landmark. Get this right and most of the work is done.

2. Name and role. The accessible name (how AT pronounces it), the role (when it’s not implicit), and any description that adds non-obvious context. Visible label preferred over aria-label whenever possible.

3. Focus and keyboard. The visible focus indicator, the tab order, the activation keys, the escape route, and — critically — where focus lands after every state transition.

4. State and updates. Which states exist (idle, loading, error, success, partial, empty, denied, stale), what each state announces, and via which live region politeness level.

5. Reading and reach. Alt text for meaningful images, captions and transcripts for media, hit target sizes for touch, motion-reduction behavior, and color-independent meaning.

Reusable template

Copy this block into your spec. Fill in every row. Put n/a where it genuinely does not apply.

### Component: <component name>

**1. Semantics**
- HTML/platform primitive: <e.g. button, dialog, listbox, region>
- Landmark or grouping: <e.g. main, nav, complementary, none>

**2. Name and role**
- Accessible name: "<exact string AT announces>"
- Visible label: <text on screen, or "none — name comes from <source>">
- Role (if not implicit): <e.g. dialog, listbox, tab, none>
- Description (aria-describedby): "<short string, or n/a>"

**3. Focus and keyboard**
- Focus indicator: <visible treatment — outline, ring, shape>
- Initial focus on open/render: <element>
- Tab order: <element 1> → <element 2> → <element 3>
- Activation keys: <Enter, Space, Esc, ArrowKeys, etc.>
- Focus on close/dismiss: <returns to <element>>
- Focus trap: <yes / no / scoped>

**4. State and updates**
- States: <list every visual state this component can render>
- Live region announcements:
  - <state X> → polite | assertive | none → "<exact text announced>"
- Errors: <how communicated visually AND to AT>

**5. Reading and reach**
- Alt text strategy: <decorative=empty alt | meaningful=description | functional=action verb>
- Captions / transcripts: <for media, n/a otherwise>
- Touch target: <≥ 44×44 logical pixels — confirmed>
- Motion: <respects prefers-reduced-motion: yes/no, fallback described>
- Color meaning: <never the only signal — paired with <icon/text/shape>>
- Zoom / reflow: <works to 200% without horizontal scroll: yes>

For full pages, add at the top:

### Page: <page name>

- Page title: "<browser tab title — front-loaded with unique info>"
- H1: "<single h1 per page>"
- Heading outline: <h1 → h2 → h2 → h3 — should make sense read alone>
- Landmarks: <header, nav, main, aside, footer — labeled when repeated>
- Skip link: <yes — target: #main>
- Reading order: <if visual order differs from DOM order, document>

For state proliferation (where most bugs hide), use a state-by-state matrix:

StateVisual treatmentAT announcementPolitenessFocus behavior
Loading<spinner / skeleton>""polite
Empty<empty illustration + CTA>""n/a (static)
Error<inline error + retry>""assertive
Success<toast + checkmark>""polite
Partial<skeletal partial + warning>""polite
Denied""n/a (static)

AI-assisted workflow

AI is a multiplier here, not the author. It is excellent at first-drafting announcement text, generating state matrices from a component description, and catching missing annotations. It is bad at deciding what should be announced — that is product judgment.

Prompt: first-draft an annotation block

You are helping a senior product designer write accessibility annotations for a
new component. Given this component description and its visual states, produce
the annotation block in the format below. Do not invent component states I did
not list. Where a property is ambiguous, write `?? — needs decision` and a one-
line question so I can resolve it.

Component description:
<paste 3-5 sentences>

Visual states (exhaustive list):
- <state 1>
- <state 2>
- <state 3>

Use this output shape:
1. Semantics
2. Name and role
3. Focus and keyboard
4. State and updates
5. Reading and reach

Prompt: stress-test a finished annotation

Act as an accessibility reviewer. Read the annotation block below and produce
a numbered list of risks, in order of severity. For each risk, name the WCAG
2.2 success criterion it touches (or note "best practice, not WCAG"). End
with three concrete questions whose answers would close the biggest gaps.
Do not rewrite the annotation. Do not invent missing context.

<paste annotation block>

Prompt: generate the state matrix from a spec

Given the spec excerpt below, list every distinct visual state the user can
encounter. For each state produce a row in this markdown table:

| State | Visual treatment | AT announcement | Politeness | Focus behavior |

If the spec is silent on a row, write `?? — open` rather than guessing.

What AI should not do: choose announcement politeness level, decide whether an image is decorative or meaningful, decide whether a control is a link or a button. These are human calls.

Collaboration considerations

  • For PMs: annotations belong in the spec, not in a separate a11y document that nobody opens. Reference them in acceptance criteria: “Loading state announces <exact string> politely” is testable. “Accessible” is not.
  • For developers: the annotation block IS the implementation contract. If a row reads ??, the spec is not ready to build. Treat unfilled annotations the same way you’d treat a missing API field.
  • For research: the announcement text strings benefit from testing with AT users, not from designer intuition. When a state’s announcement matters (errors, async results), put it in the next research session.
  • For accessibility specialists: the annotation block gives them something concrete to review and approve against WCAG 2.2 — not a finished build, where the cost of changes is 10× higher.
  • For QA: every announcement string in the annotation is a test case. Politeness level is a test case. Focus-return after dialog close is a test case.

Common failure patterns

  1. The hidden state. A state exists in code but not in the spec — denied, stale, partial, offline — and so has no announcement, no focus behavior, and no testing. The state matrix is the antidote.
  2. aria-label on everything. Hiding visible text behind a redundant aria-label is worse than nothing — sighted screen-reader users hear something different from what they see. Prefer visible labels.
  3. Focus loss on dismiss. A dialog closes and focus lands on <body>. Every dismiss action needs a stated return target.
  4. Color as the only signal. A red border is not an error to a screen-reader, and barely an error to a colorblind user. Pair color with icon, text, or shape.
  5. Inaccessible loading. A spinner with no announcement leaves AT users wondering whether their last action did anything. Loading states announce.
  6. Toasts that disappear before they’re read. Auto-dismiss timing assumes sighted attention. AT users need a longer dwell, dismiss-on-focus-out, or a persistent log surface.
  7. The decorative image with a useless alt. An icon that decorates next to a text label should have empty alt, not “icon.”
  8. Treating annotation as a final-stage activity. If annotations show up at handoff, the design itself has already missed accessibility decisions — the wrong primitive, an ambiguous focus order, an invisible state.

Generalized example

A fictional fleet-management dashboard introduces a vehicle status drawer — a right-side overlay that opens when an operator clicks a row in a vehicles table. The drawer shows the vehicle’s current trip, last 24 hours of telemetry, and lets the operator dispatch a maintenance request.

Layer 1 — Semantics. The drawer is a dialog (it manages focus, dismisses on Esc, sits over the page). The maintenance request button inside it is a button (it triggers an action; it does not navigate). The “view full vehicle history” text is a link (it navigates).

Layer 2 — Name and role. Accessible name on the drawer: "Vehicle 4471 status". The vehicle ID is the name; “status” provides role context. No visible heading needed because the drawer’s heading already says exactly that string. The maintenance button’s accessible name is "Request maintenance for vehicle 4471" — the vehicle ID is repeated so the action is unambiguous out of context.

Layer 3 — Focus and keyboard. On open: focus moves to the drawer’s close button (so dismiss is one keystroke away). Tab order: close → heading region → trip card → telemetry expand → maintenance button → full history link. Esc closes the drawer. Focus returns to the table row the operator clicked, not to the top of the table. Focus trap is scoped to the drawer.

Layer 4 — State and updates. Six states: loading, loaded, error, stale (telemetry > 5 min old), dispatched (maintenance request submitted), dispatch-failed.

StateVisualAnnouncementPolitenessFocus
loadingskeleton”Loading vehicle 4471 status”politeunchanged
loadedfull content(silent — content is now navigable)n/aunchanged
errorerror region + retry”Could not load vehicle 4471 status. Retry available.”assertiveon retry
stalewarning chip + content”Telemetry data is more than five minutes old.”politeunchanged
dispatchedsuccess toast + button → “Dispatched""Maintenance request dispatched for vehicle 4471.”politeunchanged
dispatch-failedinline error + retry”Could not dispatch maintenance request. Retry available.”assertiveon retry button

Layer 5 — Reading and reach. Vehicle status icons are decorative (alt=""); the same status is in text adjacent. Telemetry sparkline chart has a text summary above it (“Speed averaged 47 km/h over the last hour”). Touch targets meet 44×44. The drawer supports prefers-reduced-motion (no slide animation when set). Color-coded status is paired with a shape (circle / triangle / square).

The annotation block above is what ships with the spec. A developer can implement this without one Slack message back to the designer. A11y review has a concrete artifact to approve. QA writes one test per row of the state matrix.


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.