Accessibility (a11y)
Help agents verify focus order, visible labels, and screen-reader announcements for dynamic content while implementing UI.
The SKILL should name the conformance target (e.g. WCAG 2.2 AA) and required tests: keyboard-only use, 200% zoom, and reduced-motion preference (prefers-reduced-motion); split automated axe runs from manual exploration.
Perceivable and operable: text contrast, non-text alternatives, touch target size, and visible focus (:focus-visible); do not rely on color alone for state—pair changes with text or icon semantics.
ARIA Attributes and Focus Trap Implementation
Correct usage of ARIA attributes:
<!-- ✅ Form error: aria-invalid + aria-describedby -->
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
aria-required="true"
/>
<span id="email-error" role="alert">Please enter a valid email address</span>
<!-- ✅ Dynamic content: aria-live -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
<!-- auto-announced when search result count updates -->
Found {{ resultCount }} results
</div>
<!-- ✅ Icon button: aria-label -->
<button type="button" aria-label="Close dialog">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
Modal focus trap implementation:
// utils/focusTrap.ts
export function createFocusTrap(container: HTMLElement) {
const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
function getFocusable() {
return Array.from(container.querySelectorAll<HTMLElement>(focusable))
.filter(el => !el.hasAttribute('disabled'))
}
function handleKeydown(e: KeyboardEvent) {
if (e.key !== 'Tab') return
const els = getFocusable()
if (!els.length) return
const first = els[0], last = els[els.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
return {
activate() {
container.addEventListener('keydown', handleKeydown)
getFocusable()[0]?.focus() // auto-focus first interactive element
},
deactivate() {
container.removeEventListener('keydown', handleKeydown)
},
}
}
- Associate errors with controls via
aria-describedby/aria-invalid, not color alone. - On dialog close, return focus to the trigger element (
triggerEl.focus()). - When custom controls use roving tabindex, document the interaction contract in the SKILL.
Semantic HTML Comparison and jest-axe Automated Testing
Semantic HTML comparison (div+onclick vs. correct elements):
<!-- ❌ Anti-pattern: div simulating a button -->
<div class="btn" onclick="submit()">Submit</div>
<!-- Problem: no keyboard access, no role, not recognized by screen readers -->
<!-- ✅ Correct: native button element -->
<button type="submit">Submit</button>
<!-- Automatic: keyboard Enter/Space, role="button", focus management -->
<!-- ❌ Anti-pattern: span simulating a link -->
<span class="link" onclick="navigate('/about')">About us</span>
<!-- ✅ Correct: anchor element -->
<a href="/about">About us</a>
<!-- Supports: right-click to open in new tab, screen reader link list, etc. -->
jest-axe automated testing setup:
// jest.setup.ts
import { toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
// __tests__/Button.test.tsx
import { render } from '@testing-library/react'
import { axe } from 'jest-axe'
import { Button } from '@/components/Button'
test('Button has no accessibility violations', async () => {
const { container } = render(
<Button onClick={() => {}}>Submit</Button>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
// package.json configuration
// "jest-axe": "^9.0.0"
// "@testing-library/jest-dom": "^6.0.0"
Screen reader testing checklist (5 scenarios to verify with VoiceOver/NVDA):
- Scenario 1: Complete the full form submission flow using only Tab (fill → validate → submit).
- Scenario 2: Open a modal → perform an action → close; verify focus returns correctly to the trigger element.
- Scenario 3: Dynamic content updates (search results / notifications) are correctly announced by
aria-live. - Scenario 4: Images / charts have alternative text; decorative images use
alt=""to silence screen readers. - Scenario 5: Heading levels are sequential (H1→H2→H3); navigation landmarks
main/nav/footerare all present.
Agent walkthrough flow
Encode “level and scope → automation → keyboard and zoom → record exceptions” in the SKILL for ordered execution.
[ SKILL: WCAG version + level (e.g. 2.2 AA) + scope (page/component) ]
│
▼
[ Automation: axe or similar; thresholds and baseline policy ]
│
▼
[ Manual: keyboard Tab/Shift+Tab, Enter/Space; 200% zoom ]
│
▼
[ Dynamic: dialogs/routes — focus trap and return; live regions ]
│
▼
[ Output: issue list + tickets/workarounds when third parties block fixes ]
On-page tool: WCAG acceptance summary
The script below runs only here; check items to generate a list you can paste into a SKILL or review notes.
Align SKILL with walkthrough
Select items covered by the SKILL or this iteration’s acceptance criteria, then generate a summary.
SKILL outline
---
name: accessibility-a11y
description: Check front-end accessibility, keyboard use, and screen-reader experience against WCAG
---
# Rules
- Use semantic HTML: button/a/input instead of div+onclick
- All interactive elements have an accessible name (visible text, aria-label, or aria-labelledby)
- Text contrast ≥ 4.5:1; large text ≥ 3:1 (verify with a color contrast checker)
- Dynamic content updates: use aria-live="polite" or role="alert"
- Modals: move focus in on open, restore to trigger on close, Tab cycle must not escape
# Steps
1. Install jest-axe: npm i -D jest-axe @types/jest-axe
2. jest.setup.ts: import { toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations)
3. Add axe(container) assertion to every component test (CI blocks on violations)
4. Keyboard walkthrough: Tab/Shift+Tab through all controls, Enter/Space to activate, Escape to close modal
5. VoiceOver (macOS): Cmd+F5 to enable, VO+Right to navigate element by element; NVDA (Windows): NVDA+Space to browse
6. Zoom 200%: content does not overflow or overlap; prefers-reduced-motion: animations downgrade
7. Output: violation list grouped by WCAG success criterion number, with fix suggestions and priorities