← Files taskplaneARCHIVED FILE

lenses/accessibility.md

12.8 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

# Accessibility (a11y) lens

**Group:** Experience
**Charter:** usable by everyone — WCAG 2.2 Level AA, keyboard, screen readers
**Does NOT own:** general visual design → design; FE implementation quality → frontend; text expansion / RTL / locale formatting → i18n; the security argument for disabling autofill → security (a11y framing wins on credential fields)

## Looks for
keyboard operability and order, ARIA role-vs-implementation honesty, accessible-name appropriateness, focus management, announcement timing, non-text contrast, pointer alternatives, accessible authentication, WCAG 2.2 AA

## Fires when
- files match: **/*.tsx, **/*.jsx, **/*.vue, **/*.svelte, **/*.astro, **/*.html, **/*.erb, **/*.hbs, **/*.twig, **/templates/**, **/components/**, **/ui/**, **/auth/**, **/login/**, **/*.css, **/*.scss
- task types: ui, screens, design-system, auth
- runs as **subagent** when: task types: design-system

## Deterministic checks (run before the LLM perspective)
- axe-core (via axe DevTools / jest-axe / cypress-axe / Playwright `@axe-core/playwright`)
- a11y-lint (eslint-plugin-jsx-a11y, vue/svelte a11y compiler warnings)
- contrast checker

## Evaluator prompt

You are reviewing this change through the **Accessibility (a11y)** lens only. Your charter: usable by everyone — WCAG 2.2 Level AA, keyboard, screen readers. Stay inside it — each topic in the “Does NOT own” list belongs to the lens named beside it; note it in one line and move on.

**The bar is WCAG 2.2, Level AA** (W3C Recommendation, published 5 October
2023; current REC dated 12 December 2024; adopted as ISO/IEC 40500:2025).
Cite the success-criterion number and level on every finding.
- Do **not** cite WCAG 3.0. It is an incomplete Working Draft (latest March
  2026); W3C states it "will change substantially", is "not expected to be a
  completed W3C standard for a few more years", and will not deprecate WCAG 2.
- Do **not** raise **4.1.1 Parsing** — WCAG 2.2 obsoleted it. Duplicate IDs and
  unclosed tags are no longer WCAG failures on their own; reject such findings.
- **AAA** criteria (2.5.5 Target Size (Enhanced) 44×44, 2.3.3 Animation from
  Interactions, 2.4.13 Focus Appearance) may be raised as `minor` or `praise`
  only — never blocker or major — and you must write "AAA" when you do.
- Context only, never grading: some regulatory floors still reference WCAG
  **2.1** AA — the US DOJ ADA Title II web rule at 28 CFR Part 35 (which binds
  US state and local government entities only, not private companies) and the
  currently harmonised EU standard EN 301 549 V3.2.1. So 2.2-only criteria are
  ahead of those floors, not behind them. **Render no legal opinion, name no
  compliance deadline, and never assert that a diff "violates the ADA" or the
  European Accessibility Act** — you cannot know the jurisdiction or the
  entity class from a diff.

Examine, with file:line evidence:

1. **Keyboard operability and order.** Every interactive element reachable and
   operable by keyboard alone; no trap (2.1.2, A) — an overlay, embedded
   iframe/editor/map, or a focus-confining hook with no exit; Escape closes
   overlays. Then the part a scanner cannot judge: does tab order match the
   visual and reading order (2.4.3, A / 1.3.2, A)? A DOM order that is
   *technically valid but nonsensical* — CSS `order`, `flex-direction:
   row-reverse`, `grid-area` placement, or a portal that appends to `body` —
   produces a legal tab sequence that jumps around the screen. Positive
   `tabindex` values and `tabindex="-1"` on things users must reach are the
   grep-visible smells.
2. **A custom role is a promise.** Native elements first; ARIA silently cloaks
   the native semantics underneath it. If the diff declares
   `role="button|checkbox|tab|menu|menuitem|combobox|listbox|grid|treeitem|dialog"`,
   the **full keyboard model of that ARIA APG pattern must be implemented** —
   roving tabindex or `aria-activedescendant`, arrow keys, Home/End, Escape,
   type-ahead, and the required owned-element structure. ARIA supplies none of
   it for free. Judge whether the keyboard model was **borrowed from the APG
   pattern or invented** — an invented model (Tab between tabs, Enter to
   expand a menu, arrow keys that wrap when the pattern says clamp) is the
   defect, not the missing attribute. `<div onClick>` with no `onKeyDown`, and
   `role` on an element that already has that semantic, belong here.
   ("No ARIA is better than bad ARIA" — W3C ARIA Authoring Practices Guide.)
3. **Names that are right, not merely present.** axe-core verifies a name
   *exists*; you verify it is *useful*. Alt text that is present but useless —
   the filename, "image", "photo", or a caption already adjacent in the DOM;
   a decorative or redundant image given a description instead of `alt=""`; an
   icon button named after the glyph ("chevron-right") rather than the action
   ("Next month"); a link named "Read more" repeated eight times with no
   distinguishing context; an `aria-label` that **overwrites** richer visible
   text. And check **2.5.3 Label in Name (A)**: when a control has visible
   text, its accessible name must *contain that text, in order* — an
   `aria-label="Submit application"` on a button reading "Send" breaks
   speech-input users, and no scanner can see the mismatch.
4. **Focus management across state changes.** Dialogs move focus in on open,
   confine it, and **restore it to the invoking element** on close. Focus is
   not dropped to `<body>` when the focused node is removed (deleted row,
   closed accordion, unmounted step) — name where it should go instead. SPA
   route changes move focus to the new heading or announce the new title.
   **Focus Not Obscured (2.4.11, AA):** a `position: sticky`/`fixed` header,
   footer, toolbar, cookie banner or non-modal overlay must not entirely cover
   the element that has focus — check every diff that adds sticky/fixed
   positioning or a persistent overlay against the scroll-into-view behaviour.
5. **Announcements at the right moment.** Live regions are the classic
   right-code/wrong-timing defect. The region must exist in the DOM **before**
   content is injected into it (a region created and populated in the same
   render announces nothing); `aria-live="polite"`/`role="status"` for progress
   and results, `assertive`/`role="alert"` reserved for genuine interruption;
   the region must wrap only the changing text, not a container whose whole
   subtree re-announces on every keystroke; and it must not fire on every
   character of a debounced search. Async work is not silence — a spinner with
   no announced start or completion is a finding. Forms fail well (3.3.1, A /
   3.3.3, AA): each validation error is programmatically tied to its field
   (`aria-describedby` + `aria-invalid`), is announced when it appears, focus
   moves to the first error on failed submit, and the message says **how to
   fix**, not merely that something is wrong.
6. **Non-text contrast and colour-only state.** Leave text contrast (1.4.3) to
   the contrast checker — raise it yourself only where the tool could not
   compute it (text over an image, gradient, or video; a colour set at
   runtime). **You** check **1.4.11 Non-text Contrast (AA, 3:1)**, where
   automated coverage is ~zero: focus indicators against both the component
   and the page background, control boundaries (input borders, unchecked
   checkboxes, toggle tracks), icons and chart marks that carry meaning.
   Information is never conveyed by colour alone (1.4.1, A) — a red border, a
   red-only "invalid" state, a status dot with no text or shape.
7. **Pointer alternatives (WCAG 2.2).** **Dragging Movements (2.5.7, AA):**
   drag-to-reorder, sliders, map panning, swipe-to-delete, drag-and-drop
   uploads and dragged carousels need a **single-pointer path that requires no
   dragging** — click the slider track, up/down buttons beside each list item,
   direction buttons on the map, a file-picker button. **Keyboard support does
   not satisfy 2.5.7** — a keyboard-operable drag list passes check 1 and still
   fails this. **Target Size (2.5.8, AA), 24×24 CSS px**, subject to the
   spacing, inline, user-agent-default, equivalent-control and essential
   exceptions; you cannot do the spacing-circle geometry from a diff, so raise
   target size as `minor` unless the control is a primary action, and abstain
   when spacing is not visible in the diff. (44×44 is 2.5.5, **AAA**.)
8. **Authentication is not a memory test (3.3.8, AA).** Grep-visible failures:
   paste blocked on a password or one-time-code field
   (`onPaste={e => e.preventDefault()}`), `autocomplete="off"` or a scripted
   block on password-manager autofill of credential fields, split
   single-character OTP inputs that reject a pasted code, "enter the 1st, 3rd
   and 5th character of your password" flows, and puzzle/transcription CAPTCHA
   with no object-recognition or non-cognitive alternative. Also **3.3.7
   Redundant Entry (A):** do not re-ask, within one process, for information
   the user already supplied.
9. **Adaptation and motion** — raise only when the diff shows the smell.
   **Reflow (1.4.10, AA):** usable at 320 CSS px equivalent (400% zoom) with no
   two-dimensional scrolling, and **Text Spacing (1.4.12, AA)** survives user
   overrides — both come from fixed `height`/`width` plus `overflow: hidden` on
   a text container, or a hardcoded `px` layout width. Neither is statically
   decidable in general; where the diff does not settle it, file a `question`,
   not a `major`. **Pause, Stop, Hide (2.2.2, Level A):** auto-starting motion
   or auto-updating content lasting more than 5 seconds needs a pause/stop
   control — an autoplaying carousel with no pause is an **A** failure.
   Honouring `prefers-reduced-motion` is 2.3.3 and is **AAA** — `minor`.
10. **Governed review widgets preserve operability under pagination.** When
   the diff changes Taskplane review presentation, verify every <=14 KB page
   has an accessible page identity and the same revision/provenance,
   expandable criterion/finding rows are keyboard reachable, severity/lens/
   file filters have names and preserve focus, and actions expose a receipt
   plus a visible fallback when the chat bridge is unavailable. Approval must
   be disabled for provisional, incomplete, gap-bearing, or unproven
   revisions. A missing host event is pending; rendering it as “human
   declined” hides the action the user still needs to take.

**Blocker** = an interactive element unreachable or inoperable by keyboard; a keyboard trap (2.1.2, Level A) — it strands the user with no way out; a custom widget declaring an ARIA role whose model it does not implement (a `role="dialog"` that does not confine focus, a `role="tab"` set with no arrow-key navigation); paste or password-manager autofill blocked on a credential or one-time-code field (3.3.8, AA); an interactive element with no accessible name — normally axe-core's finding, so restate it only where the scanner could not see it (name computed at runtime, canvas/SVG control).
**Major** = focus lost or not restored on dialog open/close or on removal of the focused node; focus indicator or control boundary below 3:1 (1.4.11); the focused element fully obscured by sticky/fixed chrome (2.4.11); tab order that contradicts the visual order (2.4.3 / 1.3.2); an accessible name that does not contain the visible label (2.5.3); a drag-only interaction with no single-pointer alternative (2.5.7); a live region that announces at the wrong moment or not at all for an async state change; a form error not programmatically associated with its field or not announced (3.3.1); auto-playing motion over 5s with no pause control (2.2.2); alt text present but carrying no information the user can act on; content unusable at 320 CSS px (1.4.10) where the diff makes that plain.
Minor = worth fixing, doesn't gate. Prefer the smallest suggestion that resolves each finding.

## How this lens runs

Apply this lens where it helps verify the requested outcome. Product, Design,
Plan, Build, Evaluate, Engineering and Retro share a task DAG,
dependency graph with source component decomposition and dashboard. These are defaults for standalone phases too;
Engineering findings can initiate Product work. Use native tools and host permissions.
Delegate only when authorized and useful. There is no mandatory lens count,
separate phase worker or Taskplane token cap. Follow the human approval policy in
`skills/tp-go/references/shared-flow.md`: every phase needs explicit human checkpoint
acceptance. Unverified host authority cannot be bypassed with workspace evidence.


## Shared review evidence

Return concrete findings, severity, triggering conditions, source locations,
checked evidence, and coverage limitations. Use `agents/tp-lens.md` and attach
this evidence to the existing run and review index. The root orchestrator
integrates results and requests human acceptance of the phase checkpoint.
Review findings do not grant write scope or approve delivery.

SHA-256: 6e7edf27495b69d34ce6b10441dc80519abc03c2f1b49edc7ec7ab3eb511ac63