Generators produce div-buttons, unlabeled inputs, and focus-order chaos by default — not because models cannot write accessible markup, but because nothing forced them to. This playbook installs the force: accessibility constraints in every instruction file, and an axe-core CI gate that AI-authored PRs cannot skip.
Tier: Core · Workflow area: Accessibility
Prevention beats audit. Where the three-layer audit (pb-29) finds issues after the fact, this playbook stops the automatable classes from being generated at all, and blocks the ones that slip through before merge.
Skip the instruction-file half if your design system's primitives already enforce semantics and your generators are configured to use only those primitives — then the CI gate alone may suffice.
.github/copilot-instructions.md, .cursor/rules/, or CLAUDE.md at repo root.@axe-core/playwright or axe-core CLI runnable against a built preview for page-level scans.Write the constraint block from your audit findings, not from a generic list. Take the recurring defect classes from your reviews and audits and turn each into an imperative rule with a correct example. A strong starting block:
## Accessibility (non-negotiable)
- Interactive elements are native elements: <button>, <a href>, <input>,
<select>. Never a div/span with onClick. If a design needs a styled
control, use our primitives (Button, Link, IconButton).
- Every input gets a programmatically associated label. Placeholder
is never the label.
- Icon-only controls get an accessible name (aria-label or visually
hidden text). Use our <VisuallyHidden> helper.
- DOM order matches visual order. Never reorder with CSS alone
(order, flex-direction: *-reverse) for content that has a reading order.
- Images: meaningful ones get descriptive alt; decorative ones get alt="".
- Custom widgets (menu, dialog, tabs, combobox) follow the ARIA APG
pattern. If unsure, use our primitive or stop and ask.
- Every new component's tests include a jest-axe assertion.
Keep it under ~30 lines. Constraint blocks that scroll get truncated out of context windows and out of attention.
Add few-shot exemplars. Instructions state rules; exemplars make generators imitate them. Reference two or three of your most accessible components explicitly: "Follow the labeling and focus-management approach in components/forms/TextField.tsx and components/overlay/Dialog.tsx." Generators copy what they are shown far more reliably than what they are told.
Install the component-level gate. Add jest-axe to the shared test setup so every rendered-component test can assert cleanliness:
import { axe } from "jest-axe";
it("has no detectable a11y violations", async () => {
const { container } = render(<CheckoutForm />);
expect(await axe(container)).toHaveNoViolations();
});
Update your test-generation instructions (and pb-22-style prompts) so every generated component test includes this assertion for each meaningful state — not just the default render.
Install the page-level gate. Component scans miss composition problems: duplicate landmarks, heading-order breaks, focus obligations between components. Run @axe-core/playwright against your preview build's key routes in CI, with a zero-new-violations policy — baseline existing debt in a tracked allowlist file, and fail the build only on regressions. Shrink the baseline on a schedule.
Make the gate unskippable and label the escape hatch. Mark both scans as required checks in branch protection. Allow rule-level exceptions only via an inline // axe-ignore: <rule> -- <reason> <ticket> convention that a CI script counts and reports weekly. An invisible exception is a silent regression channel.
Constraints shape what generators emit; gates catch what constraints miss; branch protection makes the gate real. All three together turn accessibility from a recurring review finding into a property of the pipeline.
.github/copilot-instructions.md for repo-wide rules; path-scoped *.instructions.md files to give component directories stricter rules than scripts..cursor/rules/accessibility.mdc with alwaysApply for the constraint block; attach exemplar files via the rule's referenced-files mechanism.CLAUDE.md; for agentic runs, also add a check-before-done instruction — "run npm run test:a11y and fix violations before reporting completion."@axe-core/playwright or Lighthouse CI a11y category at page level.Everything in this playbook runs locally or in your CI: axe-core, jest-axe, Playwright, instruction files. Nothing ships data to a model at gate time.
Accessible primitives are the load-bearing wall. If your Button or Dialog primitive is itself broken, constraints will faithfully reproduce the breakage at scale and the gate will flag it in every PR. Audit and fix primitives first — one fix there removes a finding class everywhere.