accessibility

exceptionless's avatarfrom exceptionless

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

⭐2.4kstars🔀513forks📁View on GitHub🕐Updated Jan 10, 2026

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.
nameAccessibility
description|
KeywordsWCAG, 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>
<!-- 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([]);
});