BlogWeb Development
Web Development

Web Accessibility (a11y) in 2026: WCAG 2.2, ARIA Patterns, and Building Inclusive Web Applications

Web accessibility isn't just compliance — it's a competitive advantage. This guide covers WCAG 2.2 requirements, common ARIA patterns, automated testing, and practical techniques for building accessible React and Next.js applications.

P

Priya Sharma

Full-Stack Developer and open-source contributor with a passion for performance and developer experience.

February 3, 2026
21 min read

One billion people worldwide live with a disability. 15% of your users navigate with screen readers, keyboard-only input, magnification software, or alternative input devices. If your website doesn't work for them, you're excluding 15% of your potential customers. In 2025, web accessibility lawsuits in the US exceeded 4,600 (up from 2,300 in 2022), with damages averaging $25,000-$75,000 per case. The European Accessibility Act (EAA), effective June 2025, requires all digital products and services sold in the EU to be accessible.

But accessibility isn't just about avoiding lawsuits. Accessible websites are better for everyone: captions help people in noisy environments, keyboard navigation helps power users, clear visual hierarchy helps everyone find information faster, and good color contrast helps everyone read better in bright sunlight. Building accessible is building better.

WCAG 2.2: What You Need to Know

The Web Content Accessibility Guidelines (WCAG) 2.2, the current standard, defines three conformance levels: A (minimum), AA (standard target for most regulations), and AAA (enhanced). Most legal requirements and corporate policies target WCAG 2.2 Level AA.

WCAG is organized around four principles (POUR): Perceivable (users can perceive content), Operable (users can interact with UI), Understandable (users can understand content and UI), and Robust (content works with assistive technologies).

Key WCAG 2.2 criteria for web developers:

1.1.1 Non-text Content (Level A): All images, icons, and non-text content must have text alternatives. Every <img> needs an alt attribute. Decorative images use alt="". Icons that convey meaning need aria-label.

1.4.3 Contrast (Level AA): Text must have a contrast ratio of at least 4.5:1 against its background (3:1 for large text). Use a contrast checker tool — don't eyeball it.

2.1.1 Keyboard (Level A): All functionality must be operable with a keyboard. No mouse-only interactions. Every interactive element must be focusable and activatable with Enter or Space.

2.4.7 Focus Visible (Level AA): Keyboard focus indicators must be visible. Never use outline: none without providing an alternative focus indicator. The default browser outline is fine — or style a custom one that's equally visible.

Common ARIA Patterns for React Components

// Accessible modal dialog
function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Save the element that had focus before modal opened
      previousFocus.current = document.activeElement as HTMLElement;
      // Move focus to the modal
      modalRef.current?.focus();

      // Trap focus inside the modal
      const handleTab = (e: KeyboardEvent) => {
        if (e.key !== 'Tab') return;
        const focusable = modalRef.current?.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        if (!focusable?.length) return;
        const first = focusable[0] as HTMLElement;
        const last = focusable[focusable.length - 1] as HTMLElement;

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

      const handleEscape = (e: KeyboardEvent) => {
        if (e.key === 'Escape') onClose();
      };

      document.addEventListener('keydown', handleTab);
      document.addEventListener('keydown', handleEscape);

      return () => {
        document.removeEventListener('keydown', handleTab);
        document.removeEventListener('keydown', handleEscape);
        // Restore focus when modal closes
        previousFocus.current?.focus();
      };
    }
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <>
      {/* Backdrop */}
      <div className="modal-backdrop" onClick={onClose}
           aria-hidden="true" />

      {/* Modal */}
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        className="modal"
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </>
  );
}

// Accessible dropdown menu
function DropdownMenu({ label, items }: DropdownProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setIsOpen(true);
        setActiveIndex(prev => Math.min(prev + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen && activeIndex >= 0) {
          items[activeIndex].onClick();
          setIsOpen(false);
        } else {
          setIsOpen(true);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setActiveIndex(-1);
        break;
    }
  };

  return (
    <div onKeyDown={handleKeyDown}>
      <button
        aria-haspopup="true"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(!isOpen)}
      >
        {label}
      </button>
      {isOpen && (
        <ul role="menu">
          {items.map((item, index) => (
            <li
              key={index}
              role="menuitem"
              tabIndex={index === activeIndex ? 0 : -1}
              aria-current={index === activeIndex ? true : undefined}
              onClick={item.onClick}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Automated Accessibility Testing

Automated tools catch 30-50% of accessibility issues (the mechanical ones: missing alt text, insufficient contrast, missing form labels). The remaining 50-70% require manual testing: logical reading order, meaningful alt text content, keyboard workflow coherence, and screen reader announcement quality.

# Tools for automated a11y testing

# 1. axe-core — the gold standard for automated testing
# In Jest:
npm install @axe-core/react jest-axe
# In test:
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
const { container } = render(<MyComponent />);
expect(await axe(container)).toHaveNoViolations();

# 2. Lighthouse in CI/CD
npx lighthouse https://yoursite.com --output=json   --only-categories=accessibility --chrome-flags="--headless"

# 3. Pa11y — command-line accessibility testing
npx pa11y https://yoursite.com --standard WCAG2AA

# 4. Playwright with axe integration
# npm install @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('accessibility', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Quick Wins for Improving Accessibility Today

1. Add skip links. Screen reader and keyboard users shouldn't have to tab through the entire navigation on every page. Add a "Skip to main content" link as the first focusable element.

2. Use semantic HTML. Use <button> for buttons (not <div onClick>), <nav> for navigation, <main> for main content, <article> for articles. Semantic HTML provides accessibility for free.

3. Label all form inputs. Every <input> needs a visible <label> with a matching htmlFor/id. Placeholder text is not a label — it disappears when the user starts typing.

4. Don't rely on color alone. "Click the red button to delete" fails for colorblind users. Add icons, text, or patterns in addition to color: "Click the 🗑️ Delete button."

5. Ensure sufficient color contrast. Use a tool like the WebAIM Contrast Checker. Test your entire color palette — not just primary text, but also secondary text, placeholders, disabled states, and links.

ZeonEdge provides accessibility audits and remediation services for web applications. We test with screen readers, keyboard navigation, and automated tools to ensure WCAG 2.2 AA compliance. Learn about our web development services.

P

Priya Sharma

Full-Stack Developer and open-source contributor with a passion for performance and developer experience.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.