← Files taskplaneARCHIVED FILE
lenses/accessibility.md
12.8 KB · Oct 2, 2026 · 00:29 UTC
# 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