accessibility
WCAG 2.2 AA accessibility standards for the Exceptionless frontend. Semantic HTML, keyboardnavigation, ARIA patterns, focus management, and form accessibility.Keywords: WCAG, accessibility, a11y, ARIA, semantic HTML, keyboard navigation, focus management,screen reader, alt text, aria-label, aria-describedby, skip links, focus trap
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for implementing and verifying WCAG 2.2 AA accessibility standards in frontend development. It offers practical code patterns for semantic HTML, keyboard navigation, ARIA implementation, and automated accessibility testing to ensure digital products are inclusive, screen-reader friendly, and compliant with global standards.
Use Cases
- Implementing keyboard-first navigation and focus management to ensure full site operability without a mouse.
- Building accessible web forms with semantic labeling, ARIA-described error handling, and intuitive validation behaviors.
- Managing dynamic content updates using ARIA live regions and expandable patterns to support screen reader users.
- Integrating automated accessibility audits into Playwright E2E testing pipelines to detect WCAG violations during the CI/CD process.
| name | Accessibility |
|---|---|
| description | | |
| Keywords | WCAG, accessibility, a11y, ARIA, semantic HTML, keyboard navigation, focus management, |
Accessibility (WCAG 2.2 AA)
Core Principles
- Semantic HTML elements and ARIA landmarks
- Keyboard-first navigation with visible focus states
- Skip links for main content in layouts
- Inclusive, people-first language
Semantic HTML
<!-- Use semantic elements -->
<header>
<nav aria-label="Main navigation">
<a href="/dashboard">Dashboard</a>
<a href="/projects">Projects</a>
</nav>
</header>
<main id="main-content">
<h1>Page Title</h1>
<section aria-labelledby="section-heading">
<h2 id="section-heading">Section Title</h2>
<article>...</article>
</section>
</main>
<footer>...</footer>
Skip Links
<!-- At top of layout -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute ...">
Skip to main content
</a>
Form Accessibility
Label Every Control
<!-- Visible label -->
<label for="email">Email address</label>
<input id="email" type="email" />
<!-- Or using aria-label for icon-only inputs -->
<input type="search" aria-label="Search events" />
Required Fields
<label for="name">Name <span aria-hidden="true">*</span></label>
<input id="name" required aria-required="true" />
Error Messages
<input
id="email"
aria-invalid={hasError}
aria-describedby={hasError ? 'email-error' : undefined}
/>
{#if hasError}
<p id="email-error" class="text-destructive">
Please enter a valid email address
</p>
{/if}
Validation Behavior
- On validation failure: focus first invalid input
- Never disable submit just to block validation
- Show inline errors linked via
aria-describedby
Keyboard Navigation
Focus Order
<!-- Natural tab order follows DOM order -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- Remove from tab order when hidden -->
<div hidden>
<button tabindex="-1">Hidden button</button>
</div>
Focus Management in Dialogs
// When dialog opens, focus first interactive element
$effect(() => {
if (open) {
dialogRef?.querySelector('input, button')?.focus();
}
});
// When dialog closes, return focus to trigger
const triggerRef = document.activeElement;
// ... on close
triggerRef?.focus();
Keyboard Shortcuts
<button
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Action
</button>
Images and Icons
Informative Images
<img src={user.avatar} alt={`Profile photo of ${user.name}`} />
Decorative Images
<img src="/decorative-pattern.svg" alt="" aria-hidden="true" />
Icon Buttons
<button aria-label="Delete event">
<TrashIcon aria-hidden="true" />
</button>
Icons with Text
<button>
<PlusIcon aria-hidden="true" />
Add Event
</button>
ARIA Patterns
Live Regions
<!-- For dynamic updates (notifications, loading states) -->
<div aria-live="polite" aria-atomic="true">
{#if loading}
Loading events...
{/if}
</div>
<!-- For urgent messages -->
<div role="alert">
Error: Failed to save changes
</div>
Expandable Content
<button
aria-expanded={isExpanded}
aria-controls="panel-content"
>
{isExpanded ? 'Collapse' : 'Expand'}
</button>
<div id="panel-content" hidden={!isExpanded}>
Panel content
</div>
Tabs
<div role="tablist" aria-label="Event tabs">
<button role="tab" aria-selected={activeTab === 'details'}>
Details
</button>
<button role="tab" aria-selected={activeTab === 'stack'}>
Stack Trace
</button>
</div>
<div role="tabpanel" aria-labelledby="details-tab">
Tab content
</div>
Color and Contrast
- Minimum contrast ratio: 4.5:1 for normal text
- 3:1 for large text and UI components
- Don't rely on color alone to convey information
<!-- ✅ Good: Icon + color + text -->
<span class="text-destructive">
<AlertIcon aria-hidden="true" />
Error: Invalid input
</span>
<!-- ❌ Bad: Color only -->
<span class="text-destructive">Invalid input</span>
Testing Accessibility
# Run axe-playwright audits in E2E tests
npm run test:e2e
// In Playwright tests
import AxeBuilder from '@axe-core/playwright';
test('page is accessible', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});