Accessibility Implementation Best Practices

1. WCAG 2.1 AA Semantic HTML5

Element Purpose WCAG Guideline Example
<header> Page/section header Info and Relationships (1.3.1) Site banner, article header
<nav> Navigation links Info and Relationships (1.3.1) Main menu, breadcrumbs
<main> Primary content Info and Relationships (1.3.1) One per page, unique content
<article> Self-contained content Info and Relationships (1.3.1) Blog post, news article
<aside> Tangentially related content Info and Relationships (1.3.1) Sidebar, related links
<footer> Page/section footer Info and Relationships (1.3.1) Copyright, contact info
Heading hierarchy <h1> to <h6> in order Info and Relationships (1.3.1) Don't skip levels (h2 → h4)

Example: WCAG 2.1 AA compliant semantic HTML structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Accessible Web Page</title>
</head>
<body>
  <!-- Skip to main content link -->
  <a href="#main-content" class="skip-link">Skip to main content</a>

  <!-- Page header with site branding -->
  <header role="banner">
    <h1>Site Name</h1>
    <p>Tagline describing the site</p>
  </header>

  <!-- Main navigation -->
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>

  <!-- Main content area -->
  <main id="main-content">
    <h2>Page Title</h2>
    
    <!-- Article section -->
    <article>
      <h3>Article Heading</h3>
      <p>Article content with <a href="#">meaningful link text</a>.</p>
      
      <section>
        <h4>Subsection Heading</h4>
        <p>Subsection content.</p>
      </section>
    </article>

    <!-- Accessible form -->
    <form action="/submit" method="post">
      <fieldset>
        <legend>Contact Information</legend>
        
        <label for="name">Name</label>
        <input type="text" id="name" name="name" required aria-required="true">
        
        <label for="email">Email</label>
        <input 
          type="email" 
          id="email" 
          name="email" 
          required 
          aria-required="true"
          aria-describedby="email-hint"
        >
        <span id="email-hint" class="hint">We'll never share your email.</span>
      </fieldset>
      
      <button type="submit">Submit</button>
    </form>

    <!-- Accessible table -->
    <table>
      <caption>Monthly Sales Data</caption>
      <thead>
        <tr>
          <th scope="col">Month</th>
          <th scope="col">Revenue</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <th scope="row">January</th>
          <td>$10,000</td>
        </tr>
      </tbody>
    </table>

    <!-- Accessible image -->
    <figure>
      <img 
        src="chart.png" 
        alt="Bar chart showing 25% increase in sales over Q1"
      >
      <figcaption>Sales growth in Q1 2025</figcaption>
    </figure>

    <!-- Decorative image (empty alt) -->
    <img src="decorative-line.png" alt="" role="presentation">
  </main>

  <!-- Sidebar content -->
  <aside aria-label="Related content">
    <h2>Related Articles</h2>
    <ul>
      <li><a href="/article1">Article 1</a></li>
      <li><a href="/article2">Article 2</a></li>
    </ul>
  </aside>

  <!-- Page footer -->
  <footer role="contentinfo">
    <p>&copy; 2025 Company Name</p>
    <nav aria-label="Footer navigation">
      <a href="/privacy">Privacy Policy</a>
      <a href="/terms">Terms of Service</a>
    </nav>
  </footer>
</body>
</html>

<!-- CSS for skip link -->
<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}
</style>

2. ARIA Labels Roles Screen Readers

ARIA Attribute Purpose Example Use Case
role Defines element's purpose role="button" Non-semantic elements as interactive
aria-label Provides accessible name aria-label="Close dialog" Icon buttons without text
aria-labelledby References element for label aria-labelledby="heading-id" Associate region with heading
aria-describedby Additional description aria-describedby="help-text" Form field hints, tooltips
aria-live Announces dynamic content aria-live="polite" Status messages, notifications
aria-hidden Hides from screen readers aria-hidden="true" Decorative icons, duplicates
aria-expanded Expandable state aria-expanded="false" Accordions, dropdowns
aria-current Current item in set aria-current="page" Active navigation link

Example: ARIA attributes for accessible components

// Icon button with aria-label
<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>

// Search form with aria-labelledby
<form role="search" aria-labelledby="search-heading">
  <h2 id="search-heading">Search Products</h2>
  <input type="search" aria-label="Search query">
  <button type="submit">Search</button>
</form>

// Form field with aria-describedby
<label for="password">Password</label>
<input 
  type="password" 
  id="password"
  aria-describedby="password-requirements"
  aria-invalid="false"
>
<div id="password-requirements">
  Must be at least 8 characters
</div>

// Accordion with aria-expanded
function Accordion() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button
        aria-expanded={isOpen}
        aria-controls="accordion-content"
        onClick={() => setIsOpen(!isOpen)}
      >
        Show Details
      </button>
      {isOpen && (
        <div id="accordion-content" role="region">
          Accordion content
        </div>
      )}
    </div>
  );
}

// Live region for notifications
<div 
  role="status" 
  aria-live="polite" 
  aria-atomic="true"
  className="sr-only"
>
  {statusMessage}
</div>

// Alert for errors
<div role="alert" aria-live="assertive">
  {errorMessage}
</div>

// Modal dialog
function Modal({ isOpen, onClose, title, children }) {
  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="dialog-title"
      hidden={!isOpen}
    >
      <h2 id="dialog-title">{title}</h2>
      <div>{children}</div>
      <button onClick={onClose} aria-label="Close dialog">
        <span aria-hidden="true">&times;</span>
      </button>
    </div>
  );
}

// Navigation with aria-current
<nav aria-label="Main navigation">
  <a href="/" aria-current="page">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

// Tabs with ARIA
function Tabs() {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div role="tablist" aria-label="Content tabs">
        <button
          role="tab"
          aria-selected={activeTab === 0}
          aria-controls="panel-0"
          id="tab-0"
          onClick={() => setActiveTab(0)}
        >
          Tab 1
        </button>
        <button
          role="tab"
          aria-selected={activeTab === 1}
          aria-controls="panel-1"
          id="tab-1"
          onClick={() => setActiveTab(1)}
        >
          Tab 2
        </button>
      </div>
      
      <div
        role="tabpanel"
        id="panel-0"
        aria-labelledby="tab-0"
        hidden={activeTab !== 0}
      >
        Panel 1 content
      </div>
      <div
        role="tabpanel"
        id="panel-1"
        aria-labelledby="tab-1"
        hidden={activeTab !== 1}
      >
        Panel 2 content
      </div>
    </div>
  );
}

// Loading state with aria-busy
<div aria-busy={isLoading} aria-live="polite">
  {isLoading ? 'Loading...' : content}
</div>

// Visually hidden but screen-reader accessible
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

<span class="sr-only">Additional context for screen readers</span>

3. Focus Management roving tabindex

Technique Implementation Description Use Case
tabindex="0" Includes in tab order Makes non-interactive elements focusable Custom widgets
tabindex="-1" Programmatically focusable Not in tab order, focus via JS Skip links, roving tabindex
Roving tabindex One item tabindex="0" Single tab stop, arrow keys navigate Toolbars, menus, grids
Focus trap Constrain focus within Prevent focus leaving modal/dialog Modals, drawers
Focus visible :focus-visible Show focus only for keyboard users Better UX than :focus
Focus restoration Return focus after action Focus trigger after closing modal Modal dialogs

Example: Focus management and roving tabindex

// Roving tabindex for toolbar
function Toolbar({ items }) {
  const [focusedIndex, setFocusedIndex] = useState(0);
  const itemRefs = useRef([]);

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

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

    e.preventDefault();
    setFocusedIndex(newIndex);
    itemRefs.current[newIndex]?.focus();
  };

  return (
    <div role="toolbar" aria-label="Text formatting">
      {items.map((item, index) => (
        <button
          key={item.id}
          ref={el => itemRefs.current[index] = el}
          tabIndex={index === focusedIndex ? 0 : -1}
          onKeyDown={(e) => handleKeyDown(e, index)}
          onFocus={() => setFocusedIndex(index)}
          aria-label={item.label}
        >
          {item.icon}
        </button>
      ))}
    </div>
  );
}

// Focus trap for modal
import { useEffect, useRef } from 'react';

function FocusTrap({ children }) {
  const trapRef = useRef(null);

  useEffect(() => {
    const trap = trapRef.current;
    if (!trap) return;

    const focusableElements = trap.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    const handleTabKey = (e) => {
      if (e.key !== 'Tab') return;

      if (e.shiftKey) {
        if (document.activeElement === firstElement) {
          e.preventDefault();
          lastElement.focus();
        }
      } else {
        if (document.activeElement === lastElement) {
          e.preventDefault();
          firstElement.focus();
        }
      }
    };

    trap.addEventListener('keydown', handleTabKey);
    firstElement?.focus();

    return () => trap.removeEventListener('keydown', handleTabKey);
  }, []);

  return <div ref={trapRef}>{children}</div>;
}

// Modal with focus management
function Modal({ isOpen, onClose, children }) {
  const previousFocus = useRef(null);
  const modalRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Save current focus
      previousFocus.current = document.activeElement;
      
      // Focus modal
      modalRef.current?.focus();
    } else {
      // Restore focus when closed
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
    >
      <FocusTrap>
        {children}
        <button onClick={onClose}>Close</button>
      </FocusTrap>
    </div>
  );
}

// Focus-visible CSS
button:focus {
  outline: none; /* Remove default */
}

button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

// Custom hook for roving tabindex
function useRovingTabIndex(size) {
  const [currentIndex, setCurrentIndex] = useState(0);

  const handleKeyDown = useCallback((e) => {
    switch (e.key) {
      case 'ArrowRight':
      case 'ArrowDown':
        e.preventDefault();
        setCurrentIndex((prev) => (prev + 1) % size);
        break;
      case 'ArrowLeft':
      case 'ArrowUp':
        e.preventDefault();
        setCurrentIndex((prev) => (prev - 1 + size) % size);
        break;
      case 'Home':
        e.preventDefault();
        setCurrentIndex(0);
        break;
      case 'End':
        e.preventDefault();
        setCurrentIndex(size - 1);
        break;
    }
  }, [size]);

  return { currentIndex, setCurrentIndex, handleKeyDown };
}

// Usage
function Menu() {
  const items = ['Item 1', 'Item 2', 'Item 3'];
  const { currentIndex, handleKeyDown } = useRovingTabIndex(items.length);

  return (
    <div role="menu">
      {items.map((item, index) => (
        <div
          key={index}
          role="menuitem"
          tabIndex={index === currentIndex ? 0 : -1}
          onKeyDown={handleKeyDown}
        >
          {item}
        </div>
      ))}
    </div>
  );
}

// Skip to main content
<a href="#main-content" className="skip-link">
  Skip to main content
</a>

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

4. Color Contrast Checker WebAIM

Level Contrast Ratio Requirement Use Case
AA Normal Text 4.5:1 Minimum for body text <18px or <14px bold
AA Large Text 3:1 Large or bold text >=18px or >=14px bold
AAA Normal Text 7:1 Enhanced contrast Higher accessibility
AAA Large Text 4.5:1 Enhanced for large text Better readability
UI Components 3:1 Form controls, icons Interactive elements
Focus Indicators 3:1 Focus state contrast Keyboard navigation

Example: Color contrast compliance and testing

/* WCAG AA Compliant Colors */

/* Good contrast examples (AA compliant) */
:root {
  --text-dark: #1a1a1a;      /* On white: 16.1:1 (AAA) */
  --text-medium: #595959;     /* On white: 7:1 (AAA) */
  --text-light: #757575;      /* On white: 4.6:1 (AA) */
  
  --primary: #0066cc;         /* On white: 4.7:1 (AA) */
  --primary-dark: #004080;    /* On white: 8.6:1 (AAA) */
  
  --success: #008000;         /* On white: 4.5:1 (AA) */
  --error: #c00000;           /* On white: 7.3:1 (AAA) */
  --warning: #c68400;         /* On white: 4.5:1 (AA) */
  
  /* Dark mode */
  --bg-dark: #1a1a1a;
  --text-dark-mode: #e6e6e6;  /* On dark: 11.6:1 (AAA) */
}

/* Bad contrast examples (WCAG fails) */
.bad-contrast {
  color: #999;                /* On white: 2.8:1 (FAIL) */
  background: #fff;
}

/* Good contrast example */
.good-contrast {
  color: #595959;             /* On white: 7:1 (AAA) */
  background: #fff;
}

/* Button with sufficient contrast */
.button {
  background: #0066cc;        /* Background color */
  color: #ffffff;             /* Text: 4.7:1 (AA) */
  border: 2px solid #004080;  /* Border: 8.6:1 (AAA) */
}

.button:focus {
  outline: 2px solid #0066cc; /* 3:1 with background (AA) */
  outline-offset: 2px;
}

/* Link colors with sufficient contrast */
a {
  color: #0066cc;             /* 4.7:1 on white (AA) */
  text-decoration: underline; /* Don't rely on color alone */
}

a:visited {
  color: #551a8b;             /* 6.4:1 on white (AA) */
}

/* Testing contrast in JavaScript */
function getContrast(foreground, background) {
  const getLuminance = (color) => {
    const rgb = color.match(/\d+/g).map(Number);
    const [r, g, b] = rgb.map(c => {
      c = c / 255;
      return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  };

  const l1 = getLuminance(foreground);
  const l2 = getLuminance(background);
  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);

  return (lighter + 0.05) / (darker + 0.05);
}

// Check if contrast meets WCAG AA
const contrastRatio = getContrast('rgb(0, 102, 204)', 'rgb(255, 255, 255)');
const meetsAA = contrastRatio >= 4.5; // true

// React component for contrast checking
function ContrastChecker({ foreground, background }) {
  const ratio = getContrast(foreground, background);
  const meetsAA = ratio >= 4.5;
  const meetsAAA = ratio >= 7;

  return (
    <div>
      <div style={{ color: foreground, background }}>
        Sample Text
      </div>
      <p>Contrast Ratio: {ratio.toFixed(2)}:1</p>
      <p>WCAG AA: {meetsAA ? '✓ Pass' : '✗ Fail'}</p>
      <p>WCAG AAA: {meetsAAA ? '✓ Pass' : '✗ Fail'}</p>
    </div>
  );
}

/* Dark mode with proper contrast */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a1a;
    --text: #e6e6e6;          /* 11.6:1 (AAA) */
    --text-muted: #b3b3b3;    /* 5.7:1 (AA) */
    --primary: #4d94ff;       /* 5.1:1 on dark (AA) */
  }

  body {
    background: var(--bg);
    color: var(--text);
  }
}

/* Don't rely on color alone - use icons/patterns */
.status-success {
  color: #008000;
  /* Add icon for non-color indicator */
}

.status-success::before {
  content: '✓';
  margin-right: 0.5rem;
}

.status-error {
  color: #c00000;
}

.status-error::before {
  content: '✗';
  margin-right: 0.5rem;
}

/* WebAIM Contrast Checker Tools */
// Online: https://webaim.org/resources/contrastchecker/
// Browser DevTools: Chrome/Edge Lighthouse, Firefox Accessibility Inspector
// VS Code Extensions: webhint, axe Accessibility Linter
// npm packages: axe-core, pa11y

5. Keyboard Navigation Testing

Key Action Expected Behavior Component
Tab Move forward Focus next interactive element All focusable elements
Shift + Tab Move backward Focus previous interactive element All focusable elements
Enter Activate Trigger button/link action Buttons, links
Space Activate Toggle checkbox, press button Buttons, checkboxes
Arrow keys Navigate within Move between items in group Radio, select, tabs, menus
Escape Close/Cancel Close modal, dismiss menu Modals, menus, tooltips
Home/End Jump to start/end First/last item in list Lists, select, inputs

Example: Keyboard navigation implementation

// Keyboard-accessible dropdown
function Dropdown({ items, label }) {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(-1);
  const buttonRef = useRef(null);
  const itemRefs = useRef([]);

  const handleKeyDown = (e) => {
    switch (e.key) {
      case 'Enter':
      case ' ':
        e.preventDefault();
        setIsOpen(!isOpen);
        break;
      case 'Escape':
        setIsOpen(false);
        buttonRef.current?.focus();
        break;
      case 'ArrowDown':
        e.preventDefault();
        if (!isOpen) {
          setIsOpen(true);
        } else {
          const next = (selectedIndex + 1) % items.length;
          setSelectedIndex(next);
          itemRefs.current[next]?.focus();
        }
        break;
      case 'ArrowUp':
        e.preventDefault();
        if (isOpen) {
          const prev = (selectedIndex - 1 + items.length) % items.length;
          setSelectedIndex(prev);
          itemRefs.current[prev]?.focus();
        }
        break;
      case 'Home':
        e.preventDefault();
        setSelectedIndex(0);
        itemRefs.current[0]?.focus();
        break;
      case 'End':
        e.preventDefault();
        setSelectedIndex(items.length - 1);
        itemRefs.current[items.length - 1]?.focus();
        break;
    }
  };

  return (
    <div>
      <button
        ref={buttonRef}
        aria-haspopup="listbox"
        aria-expanded={isOpen}
        onKeyDown={handleKeyDown}
        onClick={() => setIsOpen(!isOpen)}
      >
        {label}
      </button>
      {isOpen && (
        <ul role="listbox">
          {items.map((item, index) => (
            <li
              key={item.id}
              ref={el => itemRefs.current[index] = el}
              role="option"
              tabIndex={-1}
              aria-selected={index === selectedIndex}
              onKeyDown={handleKeyDown}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

// Keyboard navigation for tabs
function Tabs({ tabs }) {
  const [activeIndex, setActiveIndex] = useState(0);
  const tabRefs = useRef([]);

  const handleTabKeyDown = (e, index) => {
    let newIndex = index;

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

    e.preventDefault();
    setActiveIndex(newIndex);
    tabRefs.current[newIndex]?.focus();
  };

  return (
    <div>
      <div role="tablist">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            ref={el => tabRefs.current[index] = el}
            role="tab"
            tabIndex={index === activeIndex ? 0 : -1}
            aria-selected={index === activeIndex}
            aria-controls={`panel-${index}`}
            onKeyDown={(e) => handleTabKeyDown(e, index)}
            onClick={() => setActiveIndex(index)}
          >
            {tab.label}
          </button>
        ))}
      </div>
      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${index}`}
          hidden={index !== activeIndex}
          tabIndex={0}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

// Modal with keyboard handling
function Modal({ isOpen, onClose, children }) {
  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === 'Escape' && isOpen) {
        onClose();
      }
    };

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

  if (!isOpen) return null;

  return (
    <div role="dialog" aria-modal="true">
      {children}
      <button onClick={onClose}>Close (Esc)</button>
    </div>
  );
}

// Custom checkbox with keyboard support
function Checkbox({ checked, onChange, label }) {
  const handleKeyDown = (e) => {
    if (e.key === ' ') {
      e.preventDefault();
      onChange(!checked);
    }
  };

  return (
    <div
      role="checkbox"
      aria-checked={checked}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onClick={() => onChange(!checked)}
    >
      {label}
    </div>
  );
}

// Keyboard testing checklist
const keyboardTests = [
  'Can navigate all interactive elements with Tab',
  'Can operate all functionality with keyboard only',
  'Focus indicator is clearly visible',
  'Tab order follows logical sequence',
  'No keyboard traps (can escape all contexts)',
  'Arrow keys work in composite widgets',
  'Enter/Space activate buttons',
  'Escape closes modals and menus',
  'Can skip repetitive content (skip links)',
  'Custom widgets follow ARIA keyboard patterns',
];

// Testing script
function testKeyboardNavigation() {
  console.log('Keyboard Navigation Test');
  
  // Get all focusable elements
  const focusable = document.querySelectorAll(
    'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  
  console.log(`Found ${focusable.length} focusable elements`);
  
  // Check tab order
  focusable.forEach((el, i) => {
    console.log(`${i + 1}. ${el.tagName} - ${el.textContent?.trim()}`);
  });
}

6. axe-core Automated Testing

Tool Usage Description Integration
axe-core JavaScript library Automated accessibility testing engine Runtime, CI/CD
@axe-core/react React integration Logs violations in development Development mode
jest-axe Jest matcher Accessibility assertions in tests Unit/integration tests
cypress-axe Cypress plugin E2E accessibility testing End-to-end tests
axe DevTools Browser extension Manual testing in DevTools Development, QA
pa11y CLI tool Automated accessibility testing CI/CD pipelines

Example: Automated accessibility testing with axe-core

// Install packages
npm install --save-dev axe-core @axe-core/react jest-axe cypress-axe

// 1. axe-core in React (development mode)
// index.tsx or main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

if (process.env.NODE_ENV !== 'production') {
  import('@axe-core/react').then(axe => {
    axe.default(React, ReactDOM, 1000);
  });
}

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

// 2. jest-axe for unit tests
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('should not have accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  
  expect(results).toHaveNoViolations();
});

// Test specific component
test('button should be accessible', async () => {
  const { container } = render(
    <button>Click me</button>
  );
  
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

// Test with custom rules
test('form should be accessible', async () => {
  const { container } = render(<LoginForm />);
  
  const results = await axe(container, {
    rules: {
      'color-contrast': { enabled: true },
      'label': { enabled: true },
    }
  });
  
  expect(results).toHaveNoViolations();
});

// 3. cypress-axe for E2E tests
// cypress/support/e2e.ts
import 'cypress-axe';

// cypress/e2e/accessibility.cy.ts
describe('Accessibility tests', () => {
  beforeEach(() => {
    cy.visit('/');
    cy.injectAxe();
  });

  it('should not have accessibility violations on home page', () => {
    cy.checkA11y();
  });

  it('should not have violations after interaction', () => {
    cy.get('button').click();
    cy.checkA11y();
  });

  it('should check specific element', () => {
    cy.checkA11y('.navigation');
  });

  it('should check with options', () => {
    cy.checkA11y(null, {
      runOnly: {
        type: 'tag',
        values: ['wcag2a', 'wcag2aa'],
      },
    });
  });

  it('should exclude specific elements', () => {
    cy.checkA11y({
      exclude: ['.advertisement'],
    });
  });
});

// 4. Playwright with axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('should not have accessibility violations', async ({ page }) => {
  await page.goto('/');
  
  const accessibilityScanResults = await new AxeBuilder({ page })
    .analyze();
  
  expect(accessibilityScanResults.violations).toEqual([]);
});

test('should check specific section', async ({ page }) => {
  await page.goto('/');
  
  const results = await new AxeBuilder({ page })
    .include('.main-content')
    .exclude('.ads')
    .analyze();
  
  expect(results.violations).toEqual([]);
});

// 5. pa11y for CI/CD
// package.json
{
  "scripts": {
    "test:a11y": "pa11y-ci"
  }
}

// .pa11yci.json
{
  "defaults": {
    "timeout": 5000,
    "standard": "WCAG2AA",
    "runners": ["axe", "htmlcs"]
  },
  "urls": [
    "http://localhost:3000",
    "http://localhost:3000/about",
    "http://localhost:3000/contact"
  ]
}

// 6. Custom axe-core integration
import { run } from 'axe-core';

async function checkAccessibility(element) {
  try {
    const results = await run(element || document.body, {
      runOnly: {
        type: 'tag',
        values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
      },
    });

    if (results.violations.length > 0) {
      console.error('Accessibility violations:', results.violations);
      
      results.violations.forEach(violation => {
        console.group(violation.help);
        console.log('Impact:', violation.impact);
        console.log('Description:', violation.description);
        console.log('Nodes:', violation.nodes);
        console.groupEnd();
      });
    } else {
      console.log('✓ No accessibility violations found');
    }

    return results;
  } catch (error) {
    console.error('Accessibility check failed:', error);
  }
}

// Usage in development
if (process.env.NODE_ENV === 'development') {
  window.checkA11y = checkAccessibility;
  // In console: checkA11y()
}

// 7. GitHub Actions CI
// .github/workflows/accessibility.yml
name: Accessibility Tests
on: [push, pull_request]

jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npm run build
      - run: npm run test:a11y

// 8. React Testing Library with axe
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

test('modal should be accessible', async () => {
  const { container } = render(<App />);
  
  // Open modal
  await userEvent.click(screen.getByRole('button', { name: 'Open' }));
  
  // Check accessibility
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Accessibility Best Practices Summary

  • Semantic HTML - Use proper HTML5 elements (header, nav, main, article, aside, footer) with heading hierarchy
  • ARIA Attributes - Use aria-label, aria-labelledby, aria-describedby for context; aria-live for dynamic content; aria-expanded for state
  • Focus Management - Implement roving tabindex for toolbars/menus, focus traps for modals, restore focus after interactions
  • Color Contrast - WCAG AA requires 4.5:1 for normal text, 3:1 for large text and UI components; use :focus-visible for keyboard users
  • Keyboard Navigation - Tab/Shift+Tab for focus, Enter/Space to activate, Arrow keys for navigation, Escape to close, Home/End to jump
  • Automated Testing - Use axe-core in development (@axe-core/react), jest-axe for unit tests, cypress-axe for E2E, pa11y for CI/CD
  • Manual Testing - Test with keyboard only, screen readers (NVDA, JAWS, VoiceOver), axe DevTools browser extension