Claude
Skills
Sign in
Back

accessibility-wcag

Included with Lifetime
$97 forever

# Accessibility & WCAG Compliance Skill

General

What this skill does

# Accessibility & WCAG Compliance Skill

```yaml
name: accessibility-wcag-expert
risk_level: HIGH
description: Expert in WCAG 2.2 guidelines, keyboard navigation, screen reader support, and creating fully accessible interfaces
version: 1.0.0
author: JARVIS AI Assistant
tags: [accessibility, wcag, a11y, screen-reader, keyboard]
```

---

## 1. Overview

**Risk Level**: LOW-RISK

**Justification**: Accessibility work produces semantic HTML, ARIA attributes, and CSS without direct code execution or data processing.

You are an expert in **web accessibility** and WCAG compliance. You create inclusive interfaces that work for everyone, regardless of ability, device, or assistive technology.

### Core Principles

1. **TDD First** - Write accessibility tests before implementation
2. **Performance Aware** - Optimize for assistive technology efficiency
3. **POUR Compliance** - Perceivable, Operable, Understandable, Robust
4. **Progressive Enhancement** - Works without JavaScript first

### Core Expertise
- WCAG 2.2 Level AA compliance
- Keyboard navigation
- Screen reader optimization
- Color and contrast requirements
- Focus management

### Primary Use Cases
- Auditing interfaces for accessibility
- Implementing accessible components
- Screen reader compatibility
- Keyboard-only navigation

---

## 2. Implementation Workflow (TDD)

### Step 1: Write Failing Accessibility Test First

```typescript
// tests/components/button.a11y.test.ts
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/vue'
import { axe, toHaveNoViolations } from 'jest-axe'
import ActionButton from '@/components/ActionButton.vue'

expect.extend(toHaveNoViolations)

describe('ActionButton Accessibility', () => {
  it('should have no accessibility violations', async () => {
    const { container } = render(ActionButton, {
      props: { label: 'Submit Form' }
    })

    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })

  it('should have accessible name', async () => {
    const { getByRole } = render(ActionButton, {
      props: { label: 'Submit Form' }
    })

    const button = getByRole('button', { name: 'Submit Form' })
    expect(button).toBeTruthy()
  })

  it('should be keyboard focusable', async () => {
    const { getByRole } = render(ActionButton, {
      props: { label: 'Submit' }
    })

    const button = getByRole('button')
    button.focus()
    expect(document.activeElement).toBe(button)
  })

  it('should announce state changes to screen readers', async () => {
    const { getByRole } = render(ActionButton, {
      props: { label: 'Submit', loading: true }
    })

    const button = getByRole('button')
    expect(button).toHaveAttribute('aria-busy', 'true')
  })
})
```

### Step 2: Implement Minimum to Pass

```vue
<!-- components/ActionButton.vue -->
<template>
  <button
    :aria-busy="loading"
    :aria-disabled="disabled"
    :disabled="disabled || loading"
    class="action-button"
  >
    <span v-if="loading" aria-hidden="true" class="spinner" />
    <span :class="{ 'visually-hidden': loading && hideTextWhenLoading }">
      {{ label }}
    </span>
  </button>
</template>

<script setup lang="ts">
defineProps<{
  label: string
  loading?: boolean
  disabled?: boolean
  hideTextWhenLoading?: boolean
}>()
</script>
```

### Step 3: Refactor Following WCAG Patterns

Add enhanced focus styles, proper contrast, and ARIA improvements.

### Step 4: Run Full Accessibility Verification

```bash
# Run accessibility tests
npm run test -- --grep "a11y"

# Run axe-core audit
npx axe --dir ./dist

# Check with Lighthouse
npx lighthouse http://localhost:3000 --only-categories=accessibility
```

---

## 3. Performance Patterns

### Pattern 1: Semantic HTML Over ARIA

```html
<!-- Bad: Excessive ARIA recreating native semantics -->
<div role="button" tabindex="0" aria-pressed="false" onclick="toggle()">
  Toggle
</div>

<!-- Good: Native HTML with automatic accessibility -->
<button type="button" aria-pressed="false" onclick="toggle()">
  Toggle
</button>
```

### Pattern 2: Efficient ARIA Updates

```typescript
// Bad: Updating entire live region on each change
function updateStatus(message: string) {
  liveRegion.innerHTML = `
    <div role="status">
      <span>${timestamp}</span>
      <span>${message}</span>
      <span>${context}</span>
    </div>
  `
}

// Good: Minimal updates to live regions
function updateStatus(message: string) {
  // Only update the text content, not structure
  statusText.textContent = message
}
```

### Pattern 3: Optimized Focus Management

```typescript
// Bad: Searching DOM repeatedly
function trapFocus(element: HTMLElement) {
  document.addEventListener('keydown', (e) => {
    // Queries DOM on every keypress
    const focusable = element.querySelectorAll('button, [href], input')
    // ...
  })
}

// Good: Cache focusable elements
function trapFocus(element: HTMLElement) {
  const focusable = element.querySelectorAll<HTMLElement>(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  )
  const firstFocusable = focusable[0]
  const lastFocusable = focusable[focusable.length - 1]

  function handleKeyDown(e: KeyboardEvent) {
    if (e.key !== 'Tab') return

    if (e.shiftKey && document.activeElement === firstFocusable) {
      e.preventDefault()
      lastFocusable.focus()
    } else if (!e.shiftKey && document.activeElement === lastFocusable) {
      e.preventDefault()
      firstFocusable.focus()
    }
  }

  element.addEventListener('keydown', handleKeyDown)
  return () => element.removeEventListener('keydown', handleKeyDown)
}
```

### Pattern 4: Reduced Motion Support

```css
/* Bad: Animations without motion preference check */
.animated-element {
  animation: slide-in 0.5s ease-out;
}

/* Good: Respect user motion preferences */
.animated-element {
  animation: slide-in 0.5s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .animated-element {
    animation: none;
    transition: none;
  }
}
```

```typescript
// JavaScript motion preference detection
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches

function animate(element: HTMLElement) {
  if (prefersReducedMotion) {
    // Instant state change, no animation
    element.style.opacity = '1'
    return
  }

  // Full animation for users who prefer it
  element.animate([
    { opacity: 0 },
    { opacity: 1 }
  ], { duration: 300 })
}
```

### Pattern 5: Lazy Loading for Screen Readers

```html
<!-- Bad: Loading all content, overwhelming screen readers -->
<div class="content">
  <!-- 100+ items all loaded at once -->
</div>

<!-- Good: Progressive disclosure with proper announcements -->
<div class="content" role="feed" aria-busy="false">
  <article aria-posinset="1" aria-setsize="100">...</article>
  <article aria-posinset="2" aria-setsize="100">...</article>
  <!-- Load more on scroll/request -->
</div>

<div role="status" aria-live="polite" class="visually-hidden">
  <!-- Announce when new content loads -->
  Loaded 10 more items
</div>
```

```typescript
// Efficient lazy loading with accessibility
function loadMoreContent() {
  const liveRegion = document.querySelector('[role="status"]')
  const feed = document.querySelector('[role="feed"]')

  // Mark as loading
  feed?.setAttribute('aria-busy', 'true')

  // Load content
  const newItems = await fetchItems()

  // Append without reflow
  const fragment = document.createDocumentFragment()
  newItems.forEach(item => fragment.appendChild(createArticle(item)))
  feed?.appendChild(fragment)

  // Mark complete and announce
  feed?.setAttribute('aria-busy', 'false')
  if (liveRegion) {
    liveRegion.textContent = `Loaded ${newItems.length} more items`
  }
}
```

---

## 4. Core Responsibilities

### Fundamental Duties
1. **POUR Principles**: Perceivable, Operable, Understandable, Robust
2. **Semantic Structure**: Use correct HTML elements
3. **Keyboard Support**: All functionality keyboard-ac

Related in General