Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
450 changes: 450 additions & 0 deletions .agents/skills/accessibility/SKILL.md

Large diffs are not rendered by default.

233 changes: 233 additions & 0 deletions .agents/skills/accessibility/references/A11Y-PATTERNS.md
Original file line number Diff line number Diff line change
@@ -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 `<dialog>` 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
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content" tabindex="-1">
<!-- main content -->
</main>
</body>
```

```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
<form novalidate>
<div class="field" aria-live="polite">
<label for="email">Email</label>
<input type="email" id="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" class="error" role="alert">
Please enter a valid email address (e.g., name@example.com)
</p>
</div>
</form>
```

```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 `<label>`).

```html
<!-- ❌ No label association -->
<input type="email" placeholder="Email">

<!-- ✅ Explicit label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email"
autocomplete="email" required>

<!-- ✅ Implicit label -->
<label>
Email address
<input type="email" name="email" autocomplete="email" required>
</label>

<!-- ✅ With instructions -->
<label for="password">Password</label>
<input type="password" id="password"
aria-describedby="password-requirements">
<p id="password-requirements">
Must be at least 8 characters with one number.
</p>
```

---

## Dragging movements

Any action triggered by dragging must offer a single-pointer alternative (WCAG 2.5.7).

```html
<!-- ❌ Drag-only reorder -->
<ul class="sortable-list" draggable="true">
<li>Item 1</li>
<li>Item 2</li>
</ul>

<!-- ✅ Drag + button alternatives -->
<ul class="sortable-list">
<li>
<span>Item 1</span>
<button aria-label="Move Item 1 up">↑</button>
<button aria-label="Move Item 1 down">↓</button>
</li>
<li>
<span>Item 2</span>
<button aria-label="Move Item 2 up">↑</button>
<button aria-label="Move Item 2 down">↓</button>
</li>
</ul>
```

Also applies to sliders, map panning, colour pickers, and similar drag-based widgets—always provide an equivalent click/tap or keyboard path.

---

## ARIA tabs

Tabs require `role="tablist"`, `role="tab"`, and `role="tabpanel"` with proper `aria-selected`, `aria-controls`, and keyboard support.

```html
<div role="tablist" aria-label="Product information">
<button role="tab" id="tab-1" aria-selected="true"
aria-controls="panel-1">Description</button>
<button role="tab" id="tab-2" aria-selected="false"
aria-controls="panel-2" tabindex="-1">Reviews</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<!-- Panel content -->
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
<!-- Panel content -->
</div>
```

Arrow keys should move focus between tabs; the active tab receives `tabindex="0"` while inactive tabs use `tabindex="-1"`.

---

## Live regions and notifications

Use `aria-live` to announce dynamic content changes to screen readers without moving focus.

```html
<!-- Status updates (polite — waits for pause in speech) -->
<div aria-live="polite" aria-atomic="true" class="status">
<!-- Content updates announced to screen readers -->
</div>

<!-- Urgent alerts (assertive — interrupts) -->
<div role="alert" aria-live="assertive">
<!-- Interrupts current announcement -->
</div>
```

```javascript
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`);
container.textContent = '';
requestAnimationFrame(() => {
container.textContent = message;
});
}
```

Clear the container before writing to ensure the same message triggers a new announcement.

---

## Screen reader commands

Quick reference for the most common screen reader shortcuts.

| Action | VoiceOver (Mac) | NVDA (Windows) |
|--------|-----------------|----------------|
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
| Next item | VO + → | ↓ |
| Previous item | VO + ← | ↑ |
| Activate | VO + Space | Enter |
| Headings list | VO + U, then arrows | H / Shift + H |
| Links list | VO + U | K / Shift + K |
Loading
Loading