Accessibility automation & component audit
Version: 0.2.0 · Type: ✨ Feature
Spec:
.specs/2026-06-16/a11y-automation/Linear: parent DES-117; this batch falls under Phase 1/3/4 (DES-118 / DES-120 / DES-121)
Background
Upgrade accessibility in @plaud/design from "rely on Radix fallbacks + manual review" to "automatically verifiable (axe) + quantified standard (WCAG 2.1 AA) + existing inventory audited and fixed".
Toolchain / Infrastructure (Phase 1, no component behavior change)
package.json: added devDependenciesvitest-axe@0.1.0,@axe-core/playwright@4.11.3,axe-core@4.12.1; addedtest:a11yanda11y:auditscripts- Added
src/test/axe-config.ts: sharedWCAG_AA_TAGS/AXE_RUN_OPTIONS, used in all three places (vitest / Playwright / audit script) test/setup.ts: registers the vitest-axe matcher; type augmentation in the siblingtest/vitest-axe.d.tsvitest.config.ts/tsconfig.json/tsconfig.build.json: added the browser-test globsrc/**/*.ct.spec.tsxtoexclude- Unified browser tests under
playwright-ct.config.ts(testMatch*.ct.spec.tsx);test:visual/test:a11yare distinguished by the@visual/@a11ytags - Added
analyzeA11yinplaywright/axe-utils.ts - PoC: appended axe assertions to the
describe('accessibility')blocks of the Button / Dialog / Input unit tests, plus@a11yblocks in the sibling*.ct.spec.tsxfiles - Confirmed
eslint-plugin-jsx-a11yis already active ineslint.config.js
Inventory Audit (Phase 3, offline, does not touch test:run)
- Added
scripts/a11y/a11y-audit-config.ts(49 public components → render fixtures),scripts/a11y/a11y-audit.ts(batch run with happy-dom + axe),scripts/a11y/a11y-dom-setup.ts - Full audit: all 49 components rendered successfully, 47 compliant / 2 to fix; the report is output to
a11y-report/(gitignore)
Component Fixes (Phase 4)
Slider — thumb missing accessible name (aria-input-field-name, serious)
Problem: aria-label was passed through to the Radix Slider root (roleless) rather than the role="slider" Thumb, so the thumb had no accessible name (WCAG 4.1.2).
Changed Files: src/components/ui/slider.tsx, src/components/Slider/Slider.test.tsx, new src/components/Slider/Slider.ct.spec.tsx (@a11y)
Changes: ui/slider destructures aria-label / aria-labelledby and lands them on SliderPrimitive.Thumb; updated the two original assertions (root → thumb) and added an axe assertion.
Menu — menuitem missing a compliant parent (aria-required-parent, critical)
Problem: the role="menuitem" elements inside <nav> had no role="menu"/group parent.
Changed Files: src/components/Menu/Menu.tsx, src/components/Menu/Menu.test.tsx, new src/components/Menu/Menu.ct.spec.tsx (@a11y)
Changes:
- Added
role="menu"+aria-orientation="vertical"to the top-level item container - Added
aria-haspopup="menu"to submenu parent items - Added
role="none"to theMenuSubItemcollapse container (so the parent menu sees through it), androle="group"to its collapsed-content container and theMenuGroupcollapsed-content container (satisfyingaria-required-childrenand providing a compliant parent for inner menuitems) - Added axe assertions (including nested submenus)
Documentation (Phase 2)
- Anchored the "Accessibility" chapter of
docs/constitution.mdto WCAG 2.1 AA + a three-layer automation baseline - Added a11y automation testing conventions to
CLAUDE.md§9.2 - Added
docs/a11y-checklist.md(an acceptance checklist by component category) and indexed it indocs/README.md
Test File Layout (final convention, no component behavior change)
To reduce per-component file count and directory sprawl, this batch also relocated test files (existing *.visual.spec.tsx files were migrated along with them):
- Single browser-test file: each component's browser tests are unified into
[Name].ct.spec.tsx, with internaltest.describe(..., { tag: '@visual' | '@a11y' }, …)blocks (11 components total, deduped from the original 8 visual + 5 a11y). Unit tests*.test.tsx(vitest / happy-dom) remain separate (cannot be merged across runners). - Single Playwright config:
playwright-ct.config.ts(testMatch*.ct.spec.tsx);test:visual=--grep @visual,test:a11y=--grep @a11y. - Scripts split by domain:
scripts/visual/(visual-diff-config / generate-visual-gallery / ai-visual-review / api),scripts/a11y/(a11y-audit / a11y-audit-config / a11y-dom-setup). - Test support moved out of src:
src/test/→ top-leveltest/(src/reverts to pure production source); the two icon registries are renamed to self-describing namesunit-icon-registry.tsx(unit-test stub) /ct-icon-registry.tsx(CT real Figma). Knock-on:vitest.configsetupFiles,tsconfig.jsoninclude addstest,tsconfig.builddrops the now-invalidsrc/testexclude, theeslint.configstrict block includestest/**, and importer relative paths. - Further split by runner ownership:
ct-icon-registry.tsx(consumed only byplaywright/index.tsx) moves intoplaywright/, co-located with the CT harness;test/keeps only the vitest global + cross-runner-sharedsetup.ts/unit-icon-registry.tsx/axe-config.ts(these three are referenced by unit tests / CT / audit scripts, so they are neutral shared assets and do not belong to any single runner directory). The entireplaywright/directory is excluded from tsc/eslint (it imports@playwright/test), so after ct-icon-registry moves in, it is transpiled by the CT build and no longer type-checked separately. - All tests go into
__tests__/: all 74*.test.tsx/*.ct.spec.tsx/*Fixture.visual.tsxfiles were moved from the component root into their respective__tests__/(full separation of tests from source), with relative imports adjusted +1 level accordingly. The__ai-review__/screenshot artifacts remain at the component root (visual:gallerypath unchanged, the ct.spec REVIEW_DIR moved up one level). Thesrc/**globs of vitest / tsconfig / eslint recurse, so they automatically cover__tests__/with no gate-rule changes needed. - A layout cheat sheet is in
CLAUDE.md§9.