diff --git a/.agents/skills/accessibility/SKILL.md b/.agents/skills/accessibility/SKILL.md new file mode 100644 index 0000000..af2299a --- /dev/null +++ b/.agents/skills/accessibility/SKILL.md @@ -0,0 +1,440 @@ +--- +name: accessibility +description: Audit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible". +license: MIT +metadata: + author: web-quality-skills + version: "1.1" +--- + +# Accessibility (a11y) + +Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities. + +## WCAG Principles: POUR + +| Principle | Description | +|-----------|-------------| +| **P**erceivable | Content can be perceived through different senses | +| **O**perable | Interface can be operated by all users | +| **U**nderstandable | Content and interface are understandable | +| **R**obust | Content works with assistive technologies | + +## Conformance levels + +| Level | Requirement | Target | +|-------|-------------|--------| +| **A** | Minimum accessibility | Must pass | +| **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) | +| **AAA** | Enhanced accessibility | Nice to have | + +--- + +## Perceivable + +### Text alternatives (1.1) + +**Images require alt text:** +```html + + + + +Bar chart showing 40% increase in Q3 sales + + + + + +
+ 2024 market trends infographic +
+ +
+
+``` + +**Icon buttons need accessible names:** +```html + + + + + + + + +``` + +**Visually hidden class:** +```css +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +``` + +### Color contrast (1.4.3, 1.4.6) + +| Text Size | AA minimum | AAA enhanced | +|-----------|------------|--------------| +| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 | +| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 | +| UI components & graphics | 3:1 | 3:1 | + +```css +/* ❌ Low contrast (2.5:1) */ +.low-contrast { + color: #999; + background: #fff; +} + +/* ✅ Sufficient contrast (7:1) */ +.high-contrast { + color: #333; + background: #fff; +} + +/* ✅ Focus states need contrast too */ +:focus-visible { + outline: 2px solid #005fcc; + outline-offset: 2px; +} +``` + +**Don't rely on color alone:** +```html + + + + + +
+ + + + Please enter a valid email address + +
+``` + +### Media alternatives (1.2) + +```html + + + + + +
+ Transcript +

Full transcript text...

+
+``` + +--- + +## Operable + +### Keyboard accessible (2.1) + +**All functionality must be keyboard accessible:** +```javascript +// ❌ Only handles click +element.addEventListener('click', handleAction); + +// ✅ Handles both click and keyboard +element.addEventListener('click', handleAction); +element.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleAction(); + } +}); +``` + +**No keyboard traps.** Users must be able to Tab into and out of every component. Use the [modal focus trap pattern](references/A11Y-PATTERNS.md#modal-focus-trap) for dialogs—the native `` element handles this automatically. + +### Focus visible (2.4.7) + +```css +/* ❌ Never remove focus outlines */ +*:focus { outline: none; } + +/* ✅ Use :focus-visible for keyboard-only focus */ +:focus { + outline: none; +} + +:focus-visible { + outline: 2px solid #005fcc; + outline-offset: 2px; +} + +/* ✅ Or custom focus styles */ +button:focus-visible { + box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5); +} +``` + +### Focus not obscured (2.4.11) — new in 2.2 + +When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden. + +```css +/* ✅ Account for sticky headers when scrolling to focused elements */ +:target { + scroll-margin-top: 80px; +} + +/* ✅ Ensure focused items clear fixed/sticky bars */ +:focus { + scroll-margin-top: 80px; + scroll-margin-bottom: 60px; +} +``` + +### Skip links (2.4.1) + +Provide a skip link so keyboard users can bypass repetitive navigation. See the [skip link pattern](references/A11Y-PATTERNS.md#skip-link) for full markup and styles. + +### Target size (2.5.8) — new in 2.2 + +Interactive targets must be at least **24 × 24 CSS pixels** (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target. + +```css +/* ✅ Minimum target size */ +button, +[role="button"], +input[type="checkbox"] + label, +input[type="radio"] + label { + min-width: 24px; + min-height: 24px; +} + +/* ✅ Comfortable target size (recommended 44×44) */ +.touch-target { + min-width: 44px; + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; +} +``` + +### Dragging movements (2.5.7) — new in 2.2 + +Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the [dragging movements pattern](references/A11Y-PATTERNS.md#dragging-movements) for a sortable-list example. + +### Timing (2.2) + +```javascript +// Allow users to extend time limits +function showSessionWarning() { + const modal = createModal({ + title: 'Session Expiring', + content: 'Your session will expire in 2 minutes.', + actions: [ + { label: 'Extend session', action: extendSession }, + { label: 'Log out', action: logout } + ], + timeout: 120000 + }); +} +``` + +### Motion (2.3) + +```css +/* Respect reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +--- + +## Understandable + +### Page language (3.1.1) + +```html + + + + + + + +

The French word for hello is bonjour.

+``` + +### Consistent navigation (3.2.3) + +```html + + +``` + +### Consistent help (3.2.6) — new in 2.2 + +If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the **same relative order** each time. Users who rely on consistent placement shouldn't have to hunt for help on every page. + +### Form labels (3.3.2) + +Every input needs a programmatically associated label. See the [form labels pattern](references/A11Y-PATTERNS.md#form-labels) for explicit, implicit, and instructional examples. + +### Error handling (3.3.1, 3.3.3) + +Announce errors to screen readers with `role="alert"` or `aria-live`, set `aria-invalid="true"` on invalid fields, and focus the first error on submit. See the [error handling pattern](references/A11Y-PATTERNS.md#error-handling) for full markup and JS. + +### Redundant entry (3.3.7) — new in 2.2 + +Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired. + +```html + +
+ Shipping address + + +
+``` + +### Accessible authentication (3.3.8) — new in 2.2 + +Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of: +- A copy-paste or autofill mechanism is available +- An alternative method exists (e.g., passkey, SSO, email link) +- The test uses object recognition or personal content (AA only; AAA removes this exception) + +```html + + + + + + +``` + +--- + +## Robust + +### ARIA usage (4.1.2) + +**Prefer native elements:** +```html + +
Click me
+ + + + + +
Option
+ + + +``` + +**When ARIA is needed,** use the correct roles and states. See the [ARIA tabs pattern](references/A11Y-PATTERNS.md#aria-tabs) for a complete tablist example. + +### Live regions (4.1.3) + +Use `aria-live` regions to announce dynamic content changes without moving focus. See the [live regions pattern](references/A11Y-PATTERNS.md#live-regions-and-notifications) for markup and a `showNotification()` helper. + +--- + +## Testing checklist + +### Automated testing +```bash +# Lighthouse accessibility audit +npx lighthouse https://example.com --only-categories=accessibility + +# axe-core +npm install @axe-core/cli -g +axe https://example.com +``` + +### Manual testing + +- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate +- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android) +- [ ] **Zoom:** Content usable at 200% zoom +- [ ] **High contrast:** Test with Windows High Contrast Mode +- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce` +- [ ] **Focus order:** Logical and follows visual order +- [ ] **Target size:** Interactive elements meet 24×24px minimum + +See the [screen reader commands reference](references/A11Y-PATTERNS.md#screen-reader-commands) for VoiceOver and NVDA shortcuts. + +--- + +## Common issues by impact + +### Critical (fix immediately) +1. Missing form labels +2. Missing image alt text +3. Insufficient color contrast +4. Keyboard traps +5. No focus indicators + +### Serious (fix before launch) +1. Missing page language +2. Missing heading structure +3. Non-descriptive link text +4. Auto-playing media +5. Missing skip links + +### Moderate (fix soon) +1. Missing ARIA labels on icons +2. Inconsistent navigation +3. Missing error identification +4. Timing without controls +5. Missing landmark regions + +## References + +- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/) +- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) +- [Deque axe Rules](https://dequeuniversity.com/rules/axe/) +- [Web Quality Audit](../web-quality-audit/SKILL.md) +- [WCAG criteria reference](references/WCAG.md) +- [Accessibility code patterns](references/A11Y-PATTERNS.md) diff --git a/.agents/skills/accessibility/references/A11Y-PATTERNS.md b/.agents/skills/accessibility/references/A11Y-PATTERNS.md new file mode 100644 index 0000000..6d500ef --- /dev/null +++ b/.agents/skills/accessibility/references/A11Y-PATTERNS.md @@ -0,0 +1,233 @@ +# Accessibility Code Patterns + +Practical, copy-paste-ready patterns for common accessibility requirements. Each pattern is self-contained and linked from the main [SKILL.md](../SKILL.md). + +--- + +## Modal focus trap + +Trap keyboard focus inside a modal dialog so Tab/Shift+Tab cycle through its focusable elements and Escape closes it. + +```javascript +function openModal(modal) { + const focusableElements = modal.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const firstElement = focusableElements[0]; + const lastElement = focusableElements[focusableElements.length - 1]; + + modal.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + if (e.shiftKey && document.activeElement === firstElement) { + e.preventDefault(); + lastElement.focus(); + } else if (!e.shiftKey && document.activeElement === lastElement) { + e.preventDefault(); + firstElement.focus(); + } + } + if (e.key === 'Escape') { + closeModal(); + } + }); + + firstElement.focus(); +} +``` + +The native `` element handles focus trapping automatically—prefer it when browser support allows. + +--- + +## Skip link + +Allows keyboard users to bypass repetitive navigation and jump straight to main content. + +```html + + +
+
+ +
+ +``` + +```css +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: #000; + color: #fff; + padding: 8px 16px; + z-index: 100; +} + +.skip-link:focus { + top: 0; +} +``` + +--- + +## Error handling + +Announce errors to screen readers and focus the first invalid field on submit. + +```html +
+
+ + + +
+
+``` + +```javascript +form.addEventListener('submit', (e) => { + const firstError = form.querySelector('[aria-invalid="true"]'); + if (firstError) { + e.preventDefault(); + firstError.focus(); + + const errorSummary = document.getElementById('error-summary'); + errorSummary.textContent = + `${errors.length} errors found. Please fix them and try again.`; + errorSummary.focus(); + } +}); +``` + +--- + +## Form labels + +Every input needs an associated label—either explicit (`for`/`id`) or implicit (wrapping `