Claude
Skills
Sign in
Back

accessibility-auditor

Included with Lifetime
$97 forever

Audits and implements web accessibility (a11y) following WCAG 2.1 guidelines with ARIA patterns, keyboard navigation, screen reader support, and contrast checking. Use when users request "accessibility audit", "a11y review", "WCAG compliance", "screen reader support", or "keyboard navigation".

Security

What this skill does


# Accessibility Auditor

Build inclusive web experiences with WCAG 2.1 compliance and comprehensive a11y patterns.

## Core Workflow

1. **Audit existing code**: Identify accessibility issues
2. **Check WCAG compliance**: Verify against success criteria
3. **Fix semantic HTML**: Use proper elements and landmarks
4. **Add ARIA attributes**: Enhance assistive technology support
5. **Implement keyboard nav**: Ensure full keyboard accessibility
6. **Test with tools**: Automated and manual testing
7. **Verify with screen readers**: Real-world testing

## WCAG 2.1 Quick Reference

### Compliance Levels

| Level | Description | Requirement |
|-------|-------------|-------------|
| A | Minimum accessibility | Must have |
| AA | Standard compliance | Industry standard |
| AAA | Enhanced accessibility | Nice to have |

### Four Principles (POUR)

1. **Perceivable**: Content must be presentable to all senses
2. **Operable**: Interface must be navigable by all users
3. **Understandable**: Content must be clear and predictable
4. **Robust**: Content must work with assistive technologies

## Semantic HTML

### Use Proper Elements

```html
<!-- Bad: Divs for everything -->
<div class="header">
  <div class="nav">
    <div onclick="navigate()">Home</div>
  </div>
</div>

<!-- Good: Semantic elements -->
<header>
  <nav aria-label="Main navigation">
    <a href="/">Home</a>
  </nav>
</header>
```

### Document Landmarks

```html
<body>
  <header>
    <nav aria-label="Main">...</nav>
  </header>

  <main id="main-content">
    <article>
      <h1>Page Title</h1>
      <section aria-labelledby="section-heading">
        <h2 id="section-heading">Section</h2>
      </section>
    </article>
    <aside aria-label="Related content">...</aside>
  </main>

  <footer>...</footer>
</body>
```

### Heading Hierarchy

```html
<!-- Correct heading order -->
<h1>Page Title</h1>
  <h2>Section</h2>
    <h3>Subsection</h3>
    <h3>Subsection</h3>
  <h2>Section</h2>
    <h3>Subsection</h3>

<!-- Never skip levels -->
<!-- Bad: h1 → h3 (skipped h2) -->
```

## ARIA Patterns

### Buttons

```tsx
// Interactive element that looks like a button
<button type="button" onClick={handleClick}>
  Click me
</button>

// If you must use a div (avoid if possible)
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      handleClick();
    }
  }}
>
  Click me
</div>
```

### Modals / Dialogs

```tsx
// components/Modal.tsx
import { useEffect, useRef } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

export function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousActiveElement = useRef<Element | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Store current focus
      previousActiveElement.current = document.activeElement;
      // Focus modal
      modalRef.current?.focus();
      // Prevent body scroll
      document.body.style.overflow = 'hidden';
    } else {
      // Restore focus
      (previousActiveElement.current as HTMLElement)?.focus();
      document.body.style.overflow = '';
    }

    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  // Handle escape key
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && isOpen) {
        onClose();
      }
    };

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

  if (!isOpen) return null;

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center"
      role="presentation"
    >
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/50"
        onClick={onClose}
        aria-hidden="true"
      />

      {/* Modal */}
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        className="relative z-10 bg-white rounded-lg p-6 max-w-md w-full"
      >
        <h2 id="modal-title" className="text-xl font-bold">
          {title}
        </h2>

        <div className="mt-4">{children}</div>

        <button
          onClick={onClose}
          className="absolute top-4 right-4"
          aria-label="Close modal"
        >
          ×
        </button>
      </div>
    </div>
  );
}
```

### Tabs

```tsx
// components/Tabs.tsx
import { useState, useRef, KeyboardEvent } from 'react';

interface Tab {
  id: string;
  label: string;
  content: React.ReactNode;
}

export function Tabs({ tabs }: { tabs: Tab[] }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id);
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    let newIndex = index;

    switch (e.key) {
      case 'ArrowLeft':
        newIndex = index === 0 ? tabs.length - 1 : index - 1;
        break;
      case 'ArrowRight':
        newIndex = index === tabs.length - 1 ? 0 : index + 1;
        break;
      case 'Home':
        newIndex = 0;
        break;
      case 'End':
        newIndex = tabs.length - 1;
        break;
      default:
        return;
    }

    e.preventDefault();
    setActiveTab(tabs[newIndex].id);
    tabRefs.current[newIndex]?.focus();
  };

  return (
    <div>
      <div role="tablist" aria-label="Content tabs" className="flex border-b">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            ref={(el) => (tabRefs.current[index] = el)}
            role="tab"
            id={`tab-${tab.id}`}
            aria-selected={activeTab === tab.id}
            aria-controls={`panel-${tab.id}`}
            tabIndex={activeTab === tab.id ? 0 : -1}
            onClick={() => setActiveTab(tab.id)}
            onKeyDown={(e) => handleKeyDown(e, index)}
            className={`px-4 py-2 ${
              activeTab === tab.id
                ? 'border-b-2 border-blue-500'
                : 'text-gray-500'
            }`}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {tabs.map((tab) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeTab !== tab.id}
          tabIndex={0}
          className="p-4"
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}
```

### Dropdown Menu

```tsx
// components/Dropdown.tsx
import { useState, useRef, useEffect, KeyboardEvent } from 'react';

interface MenuItem {
  id: string;
  label: string;
  onClick: () => void;
}

export function Dropdown({ label, items }: { label: string; items: MenuItem[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const menuRef = useRef<HTMLUListElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        if (!isOpen) {
          setIsOpen(true);
          setActiveIndex(0);
        } else {
          setActiveIndex((prev) => (prev + 1) % items.length);
        }
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex((prev) => (prev - 1 + items.length) % items.length);
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen && activeIndex >= 0) {
          items[activeIndex].onClick();
          setIsOpen(false);
          buttonRef.current?.focus();
        } else {
          setIsOpen(true);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        buttonRef.current?.focus();
        break;
    }
  };

  // Close on outside click
  useEffect(() => {
    con

Related in Security