Claude
Skills
Sign in
Back

Accessibility Engineer

Included with Lifetime
$97 forever

Implement accessibility (a11y) best practices to make applications usable by everyone. Use when building UIs, conducting accessibility audits, or ensuring WCAG compliance. Covers screen readers, keyboard navigation, ARIA attributes, and inclusive design patterns.

Design

What this skill does


# Accessibility Engineer

Build for everyone - accessibility is not optional.

## Core Principle

**Accessibility is a civil right, not a feature.**

1 in 4 adults in the US has a disability. Accessible design benefits everyone:

- Blind users (screen readers)
- Low vision users (zoom, high contrast)
- Deaf users (captions)
- Motor disabilities (keyboard-only)
- Cognitive disabilities (clear language)
- Temporary disabilities (broken arm)
- Situational limitations (bright sunlight, noisy environment)

## WCAG Compliance Levels

**Level A:** Minimum (legal requirement)
**Level AA:** Industry standard (aim for this)
**Level AAA:** Gold standard (difficult to achieve for all content)

**Target: WCAG 2.1 AA compliance**

---

## Pillar 1: Semantic HTML

### Use the Right Elements

```jsx
// ❌ Bad: Divs for everything (no semantic meaning)
<div onClick={handleClick}>Click me</div>
<div>Menu</div>

// ✅ Good: Semantic HTML
<button onClick={handleClick}>Click me</button>
<nav>Menu</nav>
```

### Document Structure

```jsx
// ✅ Proper heading hierarchy
<h1>Page Title</h1>
  <h2>Section 1</h2>
    <h3>Subsection 1.1</h3>
  <h2>Section 2</h2>

// ❌ Bad: Skipping levels
<h1>Page Title</h1>
  <h4>Section 1</h4> // Skipped h2, h3
```

### Landmarks

```jsx
<header>
  <nav aria-label="Main navigation">
    {/* Navigation links */}
  </nav>
</header>

<main>
  <article>
    {/* Main content */}
  </article>

  <aside>
    {/* Sidebar */}
  </aside>
</main>

<footer>
  {/* Footer content */}
</footer>
```

---

## Pillar 2: Keyboard Navigation

### All Interactive Elements Must Be Keyboard Accessible

```jsx
// ✅ Button is keyboard accessible by default
<button onClick={handleClick}>Click me</button>

// ❌ Div requires extra work
<div onClick={handleClick}>Click me</div> // Can't tab to it!

// ✅ If you must use div, add keyboard support
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick()
    }
  }}
>
  Click me
</div>
```

### Tab Order

```jsx
// ✅ Natural tab order (follows DOM order)
<input />
<button>Submit</button>
<a href="/help">Help</a>

// ❌ Don't use tabIndex > 0 (breaks natural order)
<button tabIndex={5}>Button</button> // Anti-pattern!

// ✅ tabIndex=-1 to remove from tab order
<div tabIndex={-1}>Not keyboard focusable</div>
```

### Focus Management

```jsx
// Modal: Trap focus inside
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef()

  useEffect(() => {
    if (!isOpen) return

    // Focus first focusable element
    const firstFocusable = modalRef.current.querySelector('button, input, a')
    firstFocusable?.focus()

    // Trap focus
    function handleTab(e) {
      if (e.key !== 'Tab') return

      const focusableElements = modalRef.current.querySelectorAll(
        'button, input, a, [tabindex]:not([tabindex="-1"])'
      )

      const first = focusableElements[0]
      const last = focusableElements[focusableElements.length - 1]

      if (e.shiftKey) {
        if (document.activeElement === first) {
          last.focus()
          e.preventDefault()
        }
      } else {
        if (document.activeElement === last) {
          first.focus()
          e.preventDefault()
        }
      }
    }

    document.addEventListener('keydown', handleTab)
    return () => document.removeEventListener('keydown', handleTab)
  }, [isOpen])

  return isOpen ? (
    <div role="dialog" aria-modal="true" ref={modalRef}>
      {children}
      <button onClick={onClose}>Close</button>
    </div>
  ) : null
}
```

### Skip Links

```jsx
// Allow keyboard users to skip navigation
<a href="#main-content" className="skip-link">
  Skip to main content
</a>

<nav>{/* Navigation */}</nav>

<main id="main-content">
  {/* Main content */}
</main>

// CSS
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
}

.skip-link:focus {
  top: 0;
}
```

---

## Pillar 3: ARIA Attributes

### Only Use ARIA When Semantic HTML Isn't Enough

```jsx
// ✅ Semantic HTML (no ARIA needed)
<button>Click me</button>

// ❌ Unnecessary ARIA
<button role="button" aria-label="Click me">Click me</button>

// ✅ ARIA needed (custom widget)
<div role="tab" aria-selected={isActive} aria-controls="panel-1">
  Tab 1
</div>
```

### Common ARIA Attributes

**aria-label** - Provides accessible name:

```jsx
<button aria-label="Close dialog">
  <XIcon /> {/* Visual only */}
</button>

<input type="search" aria-label="Search products" />
```

**aria-labelledby** - References another element:

```jsx
<h2 id="dialog-title">Delete Account</h2>
<div role="dialog" aria-labelledby="dialog-title">
  {/* Dialog content */}
</div>
```

**aria-describedby** - Additional description:

```jsx
<input
  type="password"
  aria-describedby="password-requirements"
/>
<div id="password-requirements">
  Must be at least 8 characters
</div>
```

**aria-live** - Announce dynamic content:

```jsx
// Polite: Wait for user to finish
<div aria-live="polite">
  {itemsAddedToCart} items added to cart
</div>

// Assertive: Interrupt immediately (use sparingly)
<div aria-live="assertive" role="alert">
  Error: Payment failed
</div>
```

**aria-expanded** - Collapsible content:

```jsx
<button
  aria-expanded={isOpen}
  aria-controls="dropdown-menu"
  onClick={() => setIsOpen(!isOpen)}
>
  Menu
</button>

<div id="dropdown-menu" hidden={!isOpen}>
  {/* Menu items */}
</div>
```

**aria-hidden** - Hide from screen readers:

```jsx
// Decorative icons
<span aria-hidden="true">★</span>

// Don't hide interactive elements!
// ❌ Bad
<button aria-hidden="true">Click me</button>
```

---

## Pillar 4: Forms & Inputs

### Labels

```jsx
// ✅ Good: Explicit label
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// ✅ Good: Implicit label
<label>
  Email
  <input type="email" />
</label>

// ❌ Bad: No label (placeholder is not a label!)
<input type="email" placeholder="Email" />
```

### Error Messages

```jsx
function EmailInput({ error }) {
  const errorId = 'email-error'

  return (
    <>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        aria-invalid={!!error}
        aria-describedby={error ? errorId : undefined}
      />
      {error && (
        <div id={errorId} role="alert">
          {error}
        </div>
      )}
    </>
  )
}
```

### Required Fields

```jsx
<label htmlFor="name">
  Name <span aria-label="required">*</span>
</label>
<input id="name" type="text" required aria-required="true" />
```

---

## Pillar 5: Color & Contrast

### Minimum Contrast Ratios (WCAG AA)

- Normal text (< 18pt): 4.5:1
- Large text (≥ 18pt or 14pt bold): 3:1
- UI components: 3:1

```jsx
// ❌ Bad: Insufficient contrast
<button style={{ background: '#ddd', color: '#aaa' }}>
  Submit // 1.5:1 contrast - fails!
</button>

// ✅ Good: Sufficient contrast
<button style={{ background: '#0066cc', color: '#ffffff' }}>
  Submit // 8:1 contrast - passes!
</button>
```

### Don't Use Color Alone

```jsx
// ❌ Bad: Color only
<span style={{ color: 'red' }}>Error</span>
<span style={{ color: 'green' }}>Success</span>

// ✅ Good: Color + icon/text
<span style={{ color: 'red' }}>
  <ErrorIcon aria-hidden="true" />
  Error
</span>
```

---

## Pillar 6: Images & Media

### Alt Text

```jsx
// ✅ Informative images
<img src="chart.png" alt="Sales increased 50% in Q4" />

// ✅ Decorative images
<img src="decorative-border.png" alt="" /> // Empty alt

// ❌ Bad: No alt or redundant alt
<img src="photo.jpg" /> // Missing alt
<img src="photo.jpg" alt="Photo" /> // Useless
```

### Complex Images

```jsx
<figure>
  <img src="complex-chart.png" alt="Sales data for 2024" />
  <figcaption>
    <details>
      <summary>Detailed description</summary>
      <p>Q1: $100k, Q2: $150k, Q3: $180k, Q4: $220k. Shows 50% growth year-over-year.</p>
    </details>
  </figcaption>
</figure>
```

### Video Captions

```jsx
<video con

Related in Design