โ™ฟ Lesson 10.3: Accessibility (a11y)

Build inclusive React applications that work for everyone. Learn how to make your web applications accessible to users with disabilities through semantic HTML, ARIA attributes, keyboard navigation, and WCAG compliance.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Understand the importance of web accessibility and legal requirements
  • Use ARIA attributes correctly to enhance screen reader support
  • Write semantic HTML that provides meaning and structure
  • Implement full keyboard navigation in React components
  • Test your applications with screen readers
  • Apply WCAG 2.1 guidelines for AA and AAA compliance
  • Use focus management techniques for single-page applications

Estimated Time: 75-90 minutes

Project: Audit and fix accessibility issues in a React app

๐Ÿ“‘ In This Lesson

๐ŸŒŸ Introduction to Web Accessibility

Web accessibility (often abbreviated as a11y - "a" followed by 11 letters, then "y") means building websites and applications that can be used by everyone, including people with disabilities. This includes individuals with:

  • Visual impairments: Blindness, low vision, color blindness
  • Hearing impairments: Deafness or hearing loss
  • Motor impairments: Limited ability to use a mouse or keyboard
  • Cognitive impairments: Learning disabilities, memory issues
  • Temporary disabilities: Broken arm, situational limitations

๐Ÿ“– What is A11y?

Accessibility (a11y) is the practice of making your web applications usable by as many people as possible. It's not just about supporting people with permanent disabilitiesโ€”it also benefits users with temporary impairments, situational limitations, or those using assistive technologies.

๐ŸŒ The Statistics

Consider these important facts:

๐Ÿ’ก By the Numbers

  • 1 billion people worldwide have some form of disability (WHO)
  • 15-20% of the population has some type of disability
  • 71% of users with disabilities will leave a website that's hard to use
  • $6.9 billion in spending power from people with disabilities in the US alone
  • 97% of homepages have WCAG 2 failures (WebAIM 2023)

โš–๏ธ Legal Requirements

Accessibility isn't just good practiceโ€”it's often required by law:

Region Law/Standard Requirements
United States ADA, Section 508 Websites must be accessible to people with disabilities
European Union EN 301 549 Public sector websites must meet WCAG 2.1 Level AA
United Kingdom Equality Act 2010 Service providers must make reasonable adjustments
Canada ACA (AODA) WCAG 2.0 Level AA compliance required
Australia DDA Websites must be accessible under discrimination law

โš ๏ธ Legal Consequences

Companies have faced significant lawsuits for inaccessible websites. Notable cases include Domino's Pizza (Supreme Court case), Target ($6M settlement), and Netflix ($755K settlement). Don't wait for a lawsuitโ€”build accessibility in from the start.

๐Ÿ’ญ Why Accessibility Matters

Beyond legal compliance, there are many compelling reasons to prioritize accessibility:

๐ŸŽฏ Business Benefits

โœ… Why Companies Should Care

  • Larger market reach: Access to millions of potential customers
  • Better SEO: Semantic HTML and structure helps search engines
  • Improved usability: Accessible sites are easier for everyone to use
  • Better code quality: Accessible code tends to be cleaner and more maintainable
  • Future-proofing: Works better on diverse devices and technologies
  • Corporate responsibility: Shows commitment to inclusion and diversity

๐Ÿ‘ฅ User Benefits

Accessibility features benefit many users, not just those with disabilities:

// Example: Keyboard shortcuts help power users
// Screen reader labels help voice control users
// High contrast modes help users in bright sunlight
// Captions help users in noisy environments

// A real-world example:
interface AccessibilityBenefits {
  feature: string;
  primaryUsers: string;
  secondaryUsers: string[];
}

const benefits: AccessibilityBenefits[] = [
  {
    feature: "Keyboard navigation",
    primaryUsers: "Motor impaired users",
    secondaryUsers: ["Power users", "Users without mouse"]
  },
  {
    feature: "Video captions",
    primaryUsers: "Deaf/hard of hearing",
    secondaryUsers: ["Non-native speakers", "Users in quiet spaces", "Users in noisy environments"]
  },
  {
    feature: "Clear language",
    primaryUsers: "Cognitive disabilities",
    secondaryUsers: ["Non-native speakers", "Users under stress", "All users!"]
  },
  {
    feature: "Large click targets",
    primaryUsers: "Motor impaired users",
    secondaryUsers: ["Mobile users", "Elderly users", "Users with tremors"]
  }
];

๐Ÿ’ก The Curb Cut Effect

The "curb cut effect" refers to how accessibility features designed for people with disabilities end up helping everyone. Curb cuts were originally designed for wheelchair users but are now used by parents with strollers, delivery workers with carts, travelers with luggage, and more. The same principle applies to web accessibility!

๐Ÿ”„ The Virtuous Cycle

graph LR A[Build Accessible Features] --> B[Reach More Users] B --> C[Get More Feedback] C --> D[Improve Product] D --> E[Better User Experience] E --> A style A fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff style B fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style C fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style D fill:#4299e1,stroke:#3182ce,stroke-width:2px,color:#fff style E fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

๐Ÿ“‹ WCAG Guidelines Overview

The Web Content Accessibility Guidelines (WCAG) are the international standard for web accessibility. Published by the W3C, these guidelines help make web content more accessible to people with disabilities.

๐Ÿ“– WCAG Versions

WCAG 2.1 (June 2018) is the current standard, with WCAG 2.2 released in October 2023. WCAG 3.0 is in development. Most legal requirements reference WCAG 2.1 Level AA as the baseline.

๐ŸŽฏ The Four Principles (POUR)

WCAG is organized around four core principles. Content must be:

graph TD A[WCAG Principles] --> B[Perceivable] A --> C[Operable] A --> D[Understandable] A --> E[Robust] B --> B1[Users can perceive information] B --> B2[Not invisible to senses] C --> C1[Users can operate interface] C --> C2[Not require impossible interactions] D --> D1[Users can understand content] D --> D2[Interface operates predictably] E --> E1[Content works with assistive tech] E --> E2[Works as tech evolves] style A fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style B fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style C fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style D fill:#4299e1,stroke:#3182ce,stroke-width:2px,color:#fff style E fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

1๏ธโƒฃ Perceivable

Information and UI components must be presentable to users in ways they can perceive.

Click to see Perceivable guidelines
  • Text Alternatives: Provide text alternatives for non-text content
  • Time-based Media: Provide alternatives for time-based media
  • Adaptable: Create content that can be presented in different ways
  • Distinguishable: Make it easier to see and hear content

Examples: Alt text for images, captions for videos, sufficient color contrast

2๏ธโƒฃ Operable

UI components and navigation must be operable by all users.

Click to see Operable guidelines
  • Keyboard Accessible: Make all functionality available from keyboard
  • Enough Time: Provide users enough time to read and use content
  • Seizures: Don't design content that could cause seizures
  • Navigable: Help users navigate and find content
  • Input Modalities: Make it easier to operate via various inputs

Examples: Full keyboard support, skip links, clear focus indicators

3๏ธโƒฃ Understandable

Information and operation of the UI must be understandable.

Click to see Understandable guidelines
  • Readable: Make text content readable and understandable
  • Predictable: Make pages appear and operate in predictable ways
  • Input Assistance: Help users avoid and correct mistakes

Examples: Clear labels, consistent navigation, error suggestions

4๏ธโƒฃ Robust

Content must be robust enough to work with current and future technologies.

Click to see Robust guidelines
  • Compatible: Maximize compatibility with current and future tools
  • Parse-able: Ensure content can be reliably interpreted
  • Name, Role, Value: Ensure UI components are properly identified

Examples: Valid HTML, ARIA attributes, semantic markup

๐Ÿ“Š Conformance Levels

WCAG has three levels of conformance:

Level Description Common Use
A Basic accessibility features - minimum level Rarely sufficient; baseline requirements
AA โœจ Deals with common barriers - recommended level Most legal requirements target this level
AAA Highest level - specialized enhancements Often not feasible for entire sites

โœ… Target Level AA

WCAG 2.1 Level AA is the gold standard for most organizations. It's legally required in many jurisdictions and provides a good balance between accessibility and feasibility. Level AAA is great as a stretch goal but isn't always achievable for all content.

๐ŸŽฏ Key Success Criteria for React Developers

Here are the most important WCAG criteria that React developers should focus on:

Click to see key success criteria
  • 1.1.1 Non-text Content (A): Alt text for images
  • 1.4.3 Contrast (AA): 4.5:1 for normal text, 3:1 for large text
  • 2.1.1 Keyboard (A): All functionality via keyboard
  • 2.1.2 No Keyboard Trap (A): Users can navigate away
  • 2.4.3 Focus Order (A): Logical focus order
  • 2.4.7 Focus Visible (AA): Visible focus indicator
  • 3.2.1 On Focus (A): No context change on focus
  • 3.3.1 Error Identification (A): Errors clearly identified
  • 3.3.2 Labels or Instructions (A): Labels for inputs
  • 4.1.2 Name, Role, Value (A): Programmatically determined

๐Ÿ—๏ธ Semantic HTML in React

Semantic HTML means using the right HTML elements for their intended purpose. This provides meaning and structure that assistive technologies can understand and communicate to users.

๐Ÿ“– What is Semantic HTML?

Semantic HTML uses elements that clearly describe their meaning to both the browser and the developer. Instead of using generic <div> and <span> elements everywhere, semantic HTML uses meaningful tags like <header>, <nav>, <article>, and <footer>.

โŒ Non-Semantic vs โœ… Semantic

Let's compare non-semantic and semantic approaches:

// โŒ BAD: Non-semantic "div soup"
function BadApp() {
  return (
    <div className="app">
      <div className="header">
        <div className="logo">My Site</div>
        <div className="menu">
          <div className="link">Home</div>
          <div className="link">About</div>
          <div className="link">Contact</div>
        </div>
      </div>
      <div className="content">
        <div className="post">
          <div className="title">Blog Post</div>
          <div className="text">Post content...</div>
        </div>
      </div>
      <div className="footer">
        <div>ยฉ 2024 My Site</div>
      </div>
    </div>
  );
}

// โœ… GOOD: Semantic HTML
function GoodApp() {
  return (
    <div className="app">
      <header>
        <h1>My Site</h1>
        <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>
      </header>
      
      <main>
        <article>
          <h2>Blog Post</h2>
          <p>Post content...</p>
        </article>
      </main>
      
      <footer>
        <p>ยฉ 2024 My Site</p>
      </footer>
    </div>
  );
}

โœ… Why Semantic HTML Matters

  • Screen readers: Understand page structure and announce landmarks
  • Keyboard navigation: Can jump between sections easily
  • SEO: Search engines understand content hierarchy
  • Maintainability: Code is easier to read and understand
  • Styling: Can target elements more semantically with CSS

๐Ÿ—บ๏ธ Landmark Elements

Landmark elements define regions of your page that screen reader users can navigate to directly:

Element Purpose ARIA Equivalent
<header> Introductory content or navigational aids role="banner"
<nav> Navigation links role="navigation"
<main> Main content of the document role="main"
<aside> Tangentially related content role="complementary"
<footer> Footer information role="contentinfo"
<section> Thematic grouping of content role="region"
<article> Self-contained composition role="article"
<form> Form for user input role="form"

๐Ÿ“ Practical Example: Blog Layout

interface BlogPost {
  id: string;
  title: string;
  content: string;
  author: string;
  date: string;
}

interface BlogLayoutProps {
  posts: BlogPost[];
}

function BlogLayout({ posts }: BlogLayoutProps) {
  return (
    <>
      {/* Page header with site branding */}
      <header>
        <h1>My Tech Blog</h1>
        <p>Thoughts on web development and accessibility</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="/archive">Archive</a></li>
        </ul>
      </nav>

      {/* Main content area - must be unique per page */}
      <main>
        <h2>Recent Posts</h2>
        
        {posts.map(post => (
          <article key={post.id}>
            <header>
              {/* Article header (different from page header) */}
              <h3>{post.title}</h3>
              <p>
                By {post.author} on <time dateTime={post.date}>
                  {new Date(post.date).toLocaleDateString()}
                </time>
              </p>
            </header>
            
            <p>{post.content}</p>
            
            <footer>
              <a href={`/post/${post.id}`}>Read more</a>
            </footer>
          </article>
        ))}
      </main>

      {/* Sidebar with related content */}
      <aside aria-label="Sidebar">
        <section>
          <h2>Categories</h2>
          <ul>
            <li><a href="/category/react">React</a></li>
            <li><a href="/category/typescript">TypeScript</a></li>
            <li><a href="/category/a11y">Accessibility</a></li>
          </ul>
        </section>
      </aside>

      {/* Page footer */}
      <footer>
        <p>ยฉ 2024 My Tech Blog. All rights reserved.</p>
        <nav aria-label="Footer navigation">
          <ul>
            <li><a href="/privacy">Privacy Policy</a></li>
            <li><a href="/terms">Terms of Service</a></li>
          </ul>
        </nav>
      </footer>
    </>
  );
}

๐Ÿ’ก Landmark Navigation

Screen reader users can press keyboard shortcuts to jump directly to landmarks:

  • NVDA/JAWS: Press D to jump between landmarks
  • VoiceOver: Use the rotor (VO + U) to navigate by landmarks
  • Users can quickly skip navigation and jump straight to <main>

๐Ÿ”ค Heading Hierarchy

Proper heading structure is crucial for navigation and understanding:

// โŒ BAD: Skipped heading levels, no hierarchy
function BadHeadingStructure() {
  return (
    <div>
      <h1>Page Title</h1>
      <h4>Section Title</h4>  {/* Skipped h2 and h3! */}
      <h2>Another Section</h2>  {/* Out of order */}
      <h5>Subsection</h5>
    </div>
  );
}

// โœ… GOOD: Logical heading hierarchy
function GoodHeadingStructure() {
  return (
    <div>
      <h1>Page Title</h1>
      
      <h2>First Main Section</h2>
      <p>Content here...</p>
      
      <h3>Subsection of First Section</h3>
      <p>More specific content...</p>
      
      <h3>Another Subsection</h3>
      <p>Content here...</p>
      
      <h2>Second Main Section</h2>
      <p>Content here...</p>
      
      <h3>Subsection of Second Section</h3>
      <p>Content here...</p>
    </div>
  );
}

โš ๏ธ Heading Rules

  • Only one <h1> per page (typically the page title)
  • Don't skip heading levels (h1 โ†’ h2 โ†’ h3, not h1 โ†’ h3)
  • Headings describe content structure, not just visual styling
  • Screen readers use headings to navigate - make them meaningful

๐Ÿ”— Links vs Buttons

Use the right element for the right purpose:

// โŒ BAD: Wrong elements for the job
function BadInteractiveElements() {
  return (
    <div>
      {/* Don't use divs as buttons */}
      <div onClick={() => handleSubmit()}>Submit</div>
      
      {/* Don't use buttons as links */}
      <button onClick={() => window.location.href = '/about'}>
        About Us
      </button>
      
      {/* Don't use spans as clickable elements */}
      <span onClick={() => openModal()}>Open</span>
    </div>
  );
}

// โœ… GOOD: Right elements for the job
function GoodInteractiveElements() {
  const navigate = useNavigate();
  
  return (
    <div>
      {/* Use button for actions */}
      <button onClick={handleSubmit} type="button">
        Submit
      </button>
      
      {/* Use link for navigation */}
      <a href="/about">About Us</a>
      
      {/* Or use React Router Link */}
      <Link to="/about">About Us</Link>
      
      {/* Use button for modal trigger */}
      <button onClick={openModal} type="button">
        Open
      </button>
    </div>
  );
}
Use... When... Example
<button> Triggering an action on the same page Submit form, open modal, toggle menu
<a> Navigating to a different page or location Links to pages, sections, external sites
<input type="submit"> Submitting a form Form submission button

โœ… Key Differences

  • Links navigate to a URL (can be opened in new tabs, bookmarked)
  • Buttons perform actions (submit forms, open modals, toggle state)
  • Keyboard: Links use Enter, buttons use Enter and Space
  • Screen readers: Announce as "link" or "button" with different expectations

๐ŸŽญ ARIA Attributes Basics

ARIA (Accessible Rich Internet Applications) is a set of attributes that define ways to make web content and applications more accessible to people with disabilities. ARIA fills in the gaps where HTML semantics fall short.

๐Ÿ“– What is ARIA?

ARIA adds accessibility information to HTML elements through attributes. It helps communicate the role, state, and properties of UI components to assistive technologies. Think of ARIA as a translator between your custom widgets and screen readers.

โš–๏ธ The Five Rules of ARIA

Before using ARIA, understand these fundamental rules from the W3C:

โœ… Rule #1: Don't Use ARIA

If you can use a native HTML element or attribute, do that instead. Native HTML is always more robust and better supported. Use ARIA only when HTML doesn't provide what you need.

// โŒ BAD: Unnecessary ARIA
<div role="button" tabIndex={0} onClick={handleClick}>Click me</div>

// โœ… GOOD: Use native button
<button onClick={handleClick}>Click me</button>


// โŒ BAD: Unnecessary role
<nav role="navigation">...</nav>

// โœ… GOOD: nav element already has role="navigation"
<nav>...</nav>

๐Ÿ’ก Rule #2: Don't Change Native Semantics

Don't change the native semantics of an element unless you really have to. For example, don't make a heading into a button.

// โŒ BAD: Changing semantic meaning
<h2 role="button">Click me</h2>

// โœ… GOOD: Use proper structure
<h2>Section Title</h2>
<button>Click me</button>

๐Ÿ’ก Rule #3: All Interactive ARIA Controls Must Be Keyboard Accessible

If you add a role like button or link to a non-interactive element, you MUST make it keyboard accessible with tabIndex and keyboard event handlers.

๐Ÿ’ก Rule #4: Don't Use role="presentation" or aria-hidden="true" on Focusable Elements

Don't hide interactive elements from screen readers. Users need to know what they're focusing on.

๐Ÿ’ก Rule #5: All Interactive Elements Must Have an Accessible Name

Every interactive element must have a text label that screen readers can announce. Use visible text, aria-label, or aria-labelledby.

๐ŸŽฏ ARIA Attribute Categories

ARIA attributes fall into three main categories:

graph TD A[ARIA Attributes] --> B[Roles] A --> C[Properties] A --> D[States] B --> B1[What IS this?] B --> B2[role='button'] B --> B3[role='dialog'] C --> C1[What are its characteristics?] C --> C2[aria-label='Close'] C --> C3[aria-required='true'] D --> D1[What is its current condition?] D --> D2[aria-expanded='true'] D --> D3[aria-checked='false'] style A fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style B fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style C fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style D fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff

1๏ธโƒฃ ARIA Roles

Roles define what an element is or does:

// Common ARIA roles
interface ARIARoleExamples {
  widget: string[];  // Interactive elements
  composite: string[]; // Composite widgets
  document: string[]; // Document structure
  landmark: string[]; // Page regions
}

const roles: ARIARoleExamples = {
  // Widget roles
  widget: [
    'button',      // A clickable button
    'checkbox',    // A checkable input
    'link',        // A hyperlink
    'menuitem',    // An option in a menu
    'option',      // An item in a listbox
    'radio',       // A radio button
    'searchbox',   // A search input
    'slider',      // A slider control
    'spinbutton',  // A number input with up/down
    'switch',      // An on/off switch
    'tab',         // A tab in a tablist
    'textbox',     // A text input
  ],
  
  // Composite widget roles
  composite: [
    'combobox',    // Input + listbox combo
    'menu',        // A menu of options
    'listbox',     // A list of options
    'tablist',     // A list of tabs
    'tree',        // A tree view
  ],
  
  // Document structure roles
  document: [
    'article',     // Self-contained content
    'heading',     // A heading
    'img',         // An image
    'list',        // A list of items
    'listitem',    // An item in a list
    'row',         // A row in a table
    'table',       // A table
  ],
  
  // Landmark roles
  landmark: [
    'banner',      // Page header (use <header> instead)
    'navigation',  // Navigation (use <nav> instead)
    'main',        // Main content (use <main> instead)
    'complementary', // Aside (use <aside> instead)
    'contentinfo', // Footer (use <footer> instead)
    'form',        // Form (use <form> instead)
    'region',      // Generic landmark
    'search',      // Search functionality
  ]
};

2๏ธโƒฃ ARIA Properties

Properties describe characteristics that don't usually change:

Property Purpose Example
aria-label Provides a text label aria-label="Close dialog"
aria-labelledby References element(s) that label this one aria-labelledby="title-id"
aria-describedby References element(s) that describe this one aria-describedby="help-text"
aria-required Indicates required form field aria-required="true"
aria-placeholder Defines placeholder text aria-placeholder="Enter email"
aria-valuemin/max Defines range limits aria-valuemin="0"

3๏ธโƒฃ ARIA States

States describe conditions that change over time:

State Purpose Example
aria-expanded Indicates if element is expanded aria-expanded="true"
aria-checked Indicates checkbox/radio state aria-checked="true"
aria-hidden Hides element from screen readers aria-hidden="true"
aria-disabled Indicates element is disabled aria-disabled="true"
aria-selected Indicates selection state aria-selected="true"
aria-pressed Indicates toggle button state aria-pressed="true"
aria-invalid Indicates validation error aria-invalid="true"
aria-busy Indicates loading state aria-busy="true"

๐Ÿ“ Practical ARIA Examples

Example 1: Accessible Button Icon

// โŒ BAD: Icon button with no label
function BadIconButton() {
  return (
    <button onClick={handleDelete}>
      <TrashIcon />
    </button>
  );
}

// โœ… GOOD: Icon button with aria-label
function GoodIconButton() {
  return (
    <button 
      onClick={handleDelete}
      aria-label="Delete item"
    >
      <TrashIcon aria-hidden="true" />
    </button>
  );
}

// โœ… ALSO GOOD: Icon button with visible text
function BetterIconButton() {
  return (
    <button onClick={handleDelete}>
      <TrashIcon aria-hidden="true" />
      <span>Delete</span>
    </button>
  );
}

Example 2: Expandable Section

import { useState } from 'react';

interface AccordionProps {
  title: string;
  children: React.ReactNode;
}

function Accordion({ title, children }: AccordionProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const contentId = `accordion-content-${title.replace(/\s+/g, '-')}`;

  return (
    <div className="accordion">
      <h3>
        <button
          onClick={() => setIsExpanded(!isExpanded)}
          aria-expanded={isExpanded}
          aria-controls={contentId}
          id={`${contentId}-button`}
        >
          {title}
          <span aria-hidden="true">{isExpanded ? 'โ–ผ' : 'โ–ถ'}</span>
        </button>
      </h3>
      
      <div
        id={contentId}
        role="region"
        aria-labelledby={`${contentId}-button`}
        hidden={!isExpanded}
      >
        {children}
      </div>
    </div>
  );
}

// Usage
function AccordionDemo() {
  return (
    <Accordion title="What is accessibility?">
      <p>Accessibility means making your website usable by everyone...</p>
    </Accordion>
  );
}

โœ… Key ARIA Techniques

  • aria-expanded tells users if content is visible
  • aria-controls links the button to the content it controls
  • aria-labelledby associates the region with its button
  • hidden attribute hides content from everyone (including screen readers)
  • aria-hidden="true" hides decorative icons from screen readers

Example 3: Form Field with Error

import { useState } from 'react';

function AccessibleFormField() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');
  
  const fieldId = 'email-input';
  const errorId = 'email-error';
  const hintId = 'email-hint';

  const validateEmail = (value: string) => {
    if (!value) {
      setError('Email is required');
      return false;
    }
    if (!value.includes('@')) {
      setError('Please enter a valid email address');
      return false;
    }
    setError('');
    return true;
  };

  return (
    <div className="form-field">
      <label htmlFor={fieldId}>
        Email Address
        <span aria-label="required">*</span>
      </label>
      
      <p id={hintId} className="hint">
        We'll never share your email with anyone else.
      </p>
      
      <input
        type="email"
        id={fieldId}
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => validateEmail(email)}
        aria-required="true"
        aria-invalid={error ? 'true' : 'false'}
        aria-describedby={`${hintId} ${error ? errorId : ''}`}
      />
      
      {error && (
        <p id={errorId} className="error" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

๐Ÿ’ก Form Accessibility Features

  • htmlFor connects label to input
  • aria-required indicates required field
  • aria-invalid indicates validation state
  • aria-describedby connects hint and error text
  • role="alert" announces errors immediately

โŒจ๏ธ Keyboard Navigation

Many users navigate websites exclusively with a keyboardโ€”whether by necessity (motor impairments), preference (power users), or temporary circumstance (broken trackpad). Every interactive element must be fully keyboard accessible.

๐Ÿ“– Keyboard Navigation

Keyboard navigation allows users to interact with your website using only keyboard input. This requires proper focus management, logical tab order, and appropriate keyboard event handlers for all interactive elements.

โŒจ๏ธ Essential Keyboard Shortcuts

These are the standard keyboard interactions users expect:

Key Action Example Elements
Tab Move focus forward All interactive elements
Shift + Tab Move focus backward All interactive elements
Enter Activate element Links, buttons, form submission
Space Activate element Buttons, checkboxes, radio buttons
Esc Close/cancel Modals, dropdowns, menus
Arrow Keys Navigate within component Menus, tabs, radio groups, sliders
Home / End Jump to start/end Lists, text inputs
Page Up / Page Down Scroll by page Long lists, scrollable areas

๐Ÿ’ก Testing Keyboard Navigation

Put away your mouse and try navigating your site with only the keyboard. If you can't reach or activate something, neither can keyboard users!

  • Can you see where focus is at all times?
  • Can you reach every interactive element?
  • Can you activate buttons and links?
  • Can you escape from modals and menus?
  • Is the tab order logical?

๐ŸŽฏ Making Elements Keyboard Accessible

When you need to make a non-interactive element keyboard accessible, follow these requirements:

// โŒ BAD: Div with click handler but no keyboard support
function BadClickableDiv() {
  return (
    <div onClick={handleClick} className="card">
      Click me
    </div>
  );
}

// โœ… GOOD: Proper button element
function GoodButton() {
  return (
    <button onClick={handleClick} className="card-button">
      Click me
    </button>
  );
}

// โœ… ACCEPTABLE: Div made keyboard accessible (if button isn't possible)
function AccessibleDiv() {
  const handleKeyDown = (e: React.KeyboardEvent) => {
    // Both Enter and Space should activate
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault(); // Prevent page scroll on Space
      handleClick();
    }
  };

  return (
    <div
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      tabIndex={0}
      role="button"
      className="card"
    >
      Click me
    </div>
  );
}

โš ๏ธ Requirements for Custom Interactive Elements

  • tabIndex={0} - Makes element focusable
  • role="button" - Tells screen readers what it is
  • onKeyDown - Handle Enter and Space keys
  • onClick - Handle mouse clicks
  • Visible focus indicator (via CSS)

๐Ÿ“ Focus Indicators

Users must be able to see which element has focus. Never remove focus outlines without providing an alternative!

// โŒ BAD: Removing focus outline with no replacement
const badStyles = {
  button: {
    outline: 'none' // Never do this!
  }
};

// โœ… GOOD: Custom focus styles that are visible
const goodStyles = `
  button:focus {
    outline: 3px solid #667eea;
    outline-offset: 2px;
  }

  /* Or use box-shadow for a subtler look */
  button:focus {
    outline: none; /* Only if you provide alternative */
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.5);
  }

  /* Modern approach with :focus-visible */
  button:focus-visible {
    outline: 3px solid #667eea;
    outline-offset: 2px;
  }
`;

โœ… Focus Indicator Best Practices

  • High contrast: Minimum 3:1 contrast ratio with background
  • Visible: Must be clearly visible against all backgrounds
  • Consistent: Use the same style across your site
  • Sufficient size: At least 2px thick or equivalent
  • Use :focus-visible: Shows focus only for keyboard, not mouse

๐Ÿ”ข Tab Order and tabIndex

The tab order should follow the visual flow of the page. Control it with the tabIndex attribute:

tabIndex Value Meaning Use Case
tabIndex={0} Element in natural tab order Making custom elements focusable
tabIndex={-1} Focusable by script, not tab key Elements focused programmatically
tabIndex={1+} Forced tab order (anti-pattern!) Don't use positive values

โš ๏ธ Never Use Positive tabIndex Values

Positive tabIndex values (1, 2, 3, etc.) create a confusing tab order that doesn't match the visual layout. They're considered an anti-pattern and should never be used. If you need to change tab order, reorganize your DOM instead.

โŒจ๏ธ Keyboard Event Handling

Here's a reusable hook for handling keyboard interactions:

import { KeyboardEvent } from 'react';

// Custom hook for keyboard interactions
function useKeyboardInteraction(
  onActivate: () => void,
  options: {
    enableSpace?: boolean;
    enableEnter?: boolean;
    preventDefault?: boolean;
  } = {}
) {
  const {
    enableSpace = true,
    enableEnter = true,
    preventDefault = true
  } = options;

  const handleKeyDown = (e: KeyboardEvent) => {
    const isSpace = e.key === ' ' || e.key === 'Spacebar';
    const isEnter = e.key === 'Enter';

    if ((enableSpace && isSpace) || (enableEnter && isEnter)) {
      if (preventDefault) {
        e.preventDefault();
      }
      onActivate();
    }
  };

  return { onKeyDown: handleKeyDown };
}

// Usage example
function CustomButton({ onClick, children }: { 
  onClick: () => void; 
  children: React.ReactNode;
}) {
  const keyboardProps = useKeyboardInteraction(onClick);

  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onClick}
      {...keyboardProps}
      className="custom-button"
    >
      {children}
    </div>
  );
}

๐ŸŽฎ Advanced Keyboard Patterns

Some widgets require more complex keyboard interactions. Here's a tab component with proper keyboard support:

import { useState, useRef, useEffect, KeyboardEvent } from 'react';

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

interface TabsProps {
  tabs: Tab[];
  defaultTab?: string;
}

function AccessibleTabs({ tabs, defaultTab }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id);
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);

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

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

    // Focus and activate the new tab
    tabRefs.current[newIndex]?.focus();
    setActiveTab(tabs[newIndex].id);
  };

  return (
    <div className="tabs">
      {/* Tab list */}
      <div role="tablist" aria-label="Content tabs">
        {tabs.map((tab, index) => {
          const isActive = tab.id === activeTab;
          
          return (
            <button
              key={tab.id}
              ref={el => tabRefs.current[index] = el}
              role="tab"
              id={`tab-${tab.id}`}
              aria-selected={isActive}
              aria-controls={`panel-${tab.id}`}
              tabIndex={isActive ? 0 : -1}
              onClick={() => setActiveTab(tab.id)}
              onKeyDown={(e) => handleKeyDown(e, index)}
              className={isActive ? 'tab active' : 'tab'}
            >
              {tab.label}
            </button>
          );
        })}
      </div>

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

๐Ÿ’ก Keyboard Patterns in Tabs

  • Arrow Left/Right - Navigate between tabs
  • Home - Jump to first tab
  • End - Jump to last tab
  • Only active tab is in tab order (tabIndex={0})
  • Inactive tabs are focusable by script only (tabIndex={-1})
  • aria-selected indicates which tab is active

๐ŸŽฏ Focus Management in SPAs

Single-page applications (SPAs) present unique focus management challenges. When content changes without a page reload, we must manually manage focus to ensure keyboard and screen reader users aren't lost.

๐Ÿ“– Why Focus Management Matters

In traditional websites, focus automatically resets when a new page loads. In SPAs, the page doesn't reload, so focus stays where it was. Users can become disoriented if focus isn't managed properly during route changes, modal openings, or dynamic content updates.

๐Ÿ”„ Focus Management on Route Changes

When users navigate to a new route, focus should move to the new page's main content:

import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();
  const mainRef = useRef<HTMLElement>(null);

  // Focus main content area on route change
  useEffect(() => {
    mainRef.current?.focus();
  }, [location.pathname]);

  return (
    <div>
      <header>
        <nav>{/* Navigation */}</nav>
      </header>

      <main 
        ref={mainRef}
        tabIndex={-1}
        className="main-content"
      >
        {/* Route content */}
      </main>
    </div>
  );
}

โœ… Focus on Route Change

  • Use tabIndex={-1} on main content area
  • Focus it programmatically after route changes
  • This tells screen readers "you're on a new page"
  • Users can immediately read the new content

๐ŸชŸ Focus Management in Modals

Modals require careful focus management to create a proper "focus trap":

import { useEffect, useRef, KeyboardEvent } from 'react';

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

function AccessibleModal({ isOpen, onClose, title, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  // Save focus when modal opens, restore when it closes
  useEffect(() => {
    if (isOpen) {
      // Save currently focused element
      previousFocusRef.current = document.activeElement as HTMLElement;
      
      // Focus the modal
      modalRef.current?.focus();

      // Prevent body scroll
      document.body.style.overflow = 'hidden';
    } else {
      // Restore focus to element that opened modal
      previousFocusRef.current?.focus();
      
      // Restore body scroll
      document.body.style.overflow = '';
    }

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

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

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

  // Trap focus within modal
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key !== 'Tab') return;

    const focusableElements = modalRef.current?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    if (!focusableElements || focusableElements.length === 0) return;

    const firstElement = focusableElements[0] as HTMLElement;
    const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

    if (e.shiftKey && document.activeElement === firstElement) {
      // Tab backwards from first element
      e.preventDefault();
      lastElement.focus();
    } else if (!e.shiftKey && document.activeElement === lastElement) {
      // Tab forward from last element
      e.preventDefault();
      firstElement.focus();
    }
  };

  if (!isOpen) return null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="modal"
        onClick={(e) => e.stopPropagation()}
        onKeyDown={handleKeyDown}
        tabIndex={-1}
      >
        <header className="modal-header">
          <h2 id="modal-title">{title}</h2>
          <button
            onClick={onClose}
            aria-label="Close dialog"
            className="modal-close"
          >
            ร—
          </button>
        </header>

        <div className="modal-content">
          {children}
        </div>

        <footer className="modal-footer">
          <button onClick={onClose}>Cancel</button>
          <button onClick={onClose} className="primary">Confirm</button>
        </footer>
      </div>
    </div>
  );
}

๐Ÿ’ก Modal Focus Requirements

  1. Save previous focus: Remember what was focused before opening
  2. Move focus to modal: Focus the dialog element when it opens
  3. Trap focus: Keep tab navigation within the modal
  4. Handle Escape: Close modal when Escape is pressed
  5. Restore focus: Return focus to trigger element when closing
  6. Use proper ARIA: role="dialog" and aria-modal="true"

โšก Focus Management Hook

Create a reusable hook for focus management:

import { useRef, useEffect } from 'react';

// Hook to manage focus for dynamic content
function useFocusManagement(isVisible: boolean) {
  const elementRef = useRef<HTMLElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isVisible) {
      // Save current focus
      previousFocusRef.current = document.activeElement as HTMLElement;
      
      // Move focus to the element
      elementRef.current?.focus();
    } else if (previousFocusRef.current) {
      // Restore focus
      previousFocusRef.current.focus();
    }
  }, [isVisible]);

  return elementRef;
}

// Usage
function Dropdown({ isOpen, onClose }: DropdownProps) {
  const dropdownRef = useFocusManagement(isOpen);

  if (!isOpen) return null;

  return (
    <div
      ref={dropdownRef}
      tabIndex={-1}
      className="dropdown"
    >
      {/* Dropdown content */}
    </div>
  );
}

๐Ÿ“ Skip Links

Skip links allow keyboard users to bypass repetitive content:

function PageLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {/* Skip link - first focusable element */}
      <a href="#main-content" className="skip-link">
        Skip to main content
      </a>

      <header>
        <nav>
          {/* Long navigation menu */}
        </nav>
      </header>

      <main id="main-content" tabIndex={-1}>
        {children}
      </main>
    </>
  );
}

// CSS for skip link
const skipLinkStyles = `
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: #667eea;
    color: white;
    padding: 8px;
    text-decoration: none;
    z-index: 1000;
  }

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

โœ… Skip Link Best Practices

  • Position skip link as the first focusable element
  • Make it visible when focused (don't hide it completely)
  • Link to the main content area with id="main-content"
  • Make the target focusable with tabIndex={-1}
  • Consider adding multiple skip links (to navigation, search, etc.)

๐Ÿ“ข ARIA Live Regions

ARIA live regions announce dynamic content changes to screen reader users. When content updates without a page reload, screen readers won't notice unless you tell them with live regions.

๐Ÿ“– What are Live Regions?

Live regions are specially marked areas that screen readers monitor for changes. When content in a live region updates, the screen reader automatically announces the change to the user, even if they're focused elsewhere on the page.

๐Ÿ”Š ARIA Live Politeness Levels

Live regions have different politeness levels that control how urgently changes are announced:

Attribute Politeness Use Case
aria-live="off" No announcements Default - don't announce changes
aria-live="polite" Wait for pause Status messages, non-critical updates
aria-live="assertive" Interrupt immediately Errors, urgent alerts
role="status" Polite (implicit) Status messages, confirmations
role="alert" Assertive (implicit) Errors, warnings

โš ๏ธ Use Assertive Sparingly

aria-live="assertive" interrupts whatever the screen reader is currently saying. Use it only for truly urgent messages like errors or critical warnings. Overuse creates a poor experience.

๐Ÿ“ Practical Examples

Example 1: Form Validation Messages

import { useState } from 'react';

function FormWithLiveValidation() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const validateEmail = (value: string) => {
    if (!value.includes('@')) {
      setError('Please enter a valid email address');
    } else {
      setError('');
    }
  };

  return (
    <form>
      <label htmlFor="email">Email Address</label>
      <input
        type="email"
        id="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onBlur={() => validateEmail(email)}
        aria-invalid={error ? 'true' : 'false'}
        aria-describedby={error ? 'email-error' : undefined}
      />
      
      {/* Live region for error - announces immediately */}
      {error && (
        <p 
          id="email-error" 
          role="alert"
          className="error"
        >
          {error}
        </p>
      )}
    </form>
  );
}

Example 2: Loading States

function DataList() {
  const [data, setData] = useState<any[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  return (
    <div>
      <button onClick={fetchData}>Load Data</button>

      {/* Loading status - polite announcement */}
      <div 
        role="status" 
        aria-live="polite"
        aria-atomic="true"
      >
        {isLoading && 'Loading data...'}
        {!isLoading && data.length > 0 && `Loaded ${data.length} items`}
      </div>

      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Example 3: Toast Notifications

import { useState, useEffect } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'success' | 'error' | 'info';
}

function ToastContainer() {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: Toast['type']) => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type }]);

    // Auto-dismiss after 5 seconds
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, 5000);
  };

  return (
    <>
      {/* Live region for announcements */}
      <div
        role="status"
        aria-live="polite"
        aria-atomic="true"
        className="sr-only"
      >
        {toasts[toasts.length - 1]?.message}
      </div>

      {/* Visual toasts */}
      <div className="toast-container" aria-label="Notifications">
        {toasts.map(toast => (
          <div
            key={toast.id}
            className={`toast toast-${toast.type}`}
            role={toast.type === 'error' ? 'alert' : 'status'}
          >
            {toast.message}
            <button
              onClick={() => setToasts(prev => prev.filter(t => t.id !== toast.id))}
              aria-label="Dismiss notification"
            >
              ร—
            </button>
          </div>
        ))}
      </div>
    </>
  );
}

// Screen reader only styles
const srOnlyStyles = `
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
`;

โœ… Live Region Best Practices

  • Keep it brief: Announce concise, meaningful messages
  • Use aria-atomic: Set to "true" to announce entire region
  • Don't overuse: Too many announcements are overwhelming
  • Test thoroughly: Behavior varies between screen readers
  • Separate visual and announcements: Use hidden live regions
  • Prefer role="status" and role="alert": Over explicit aria-live

๐Ÿ”„ Live Region Hook

Create a reusable hook for announcements:

import { useEffect, useRef } from 'react';

function useAnnounce() {
  const liveRegionRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    // Create live region if it doesn't exist
    if (!liveRegionRef.current) {
      const liveRegion = document.createElement('div');
      liveRegion.setAttribute('role', 'status');
      liveRegion.setAttribute('aria-live', 'polite');
      liveRegion.setAttribute('aria-atomic', 'true');
      liveRegion.className = 'sr-only';
      document.body.appendChild(liveRegion);
      liveRegionRef.current = liveRegion;
    }

    return () => {
      // Cleanup on unmount
      if (liveRegionRef.current) {
        document.body.removeChild(liveRegionRef.current);
        liveRegionRef.current = null;
      }
    };
  }, []);

  const announce = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
    if (!liveRegionRef.current) return;

    // Clear previous message
    liveRegionRef.current.textContent = '';

    // Set priority
    liveRegionRef.current.setAttribute('aria-live', priority);

    // Small delay ensures screen readers notice the change
    setTimeout(() => {
      if (liveRegionRef.current) {
        liveRegionRef.current.textContent = message;
      }
    }, 100);
  };

  return announce;
}

// Usage
function SearchResults() {
  const [results, setResults] = useState<any[]>([]);
  const announce = useAnnounce();

  const handleSearch = async (query: string) => {
    const data = await fetchResults(query);
    setResults(data);
    announce(`Found ${data.length} results for ${query}`);
  };

  return (
    <div>
      {/* Search UI */}
    </div>
  );
}

๐Ÿ‹๏ธ Hands-on Exercises

Let's practice what we've learned! These exercises will help you apply accessibility concepts to real-world scenarios.

๐ŸŽฏ Exercise 1: Fix the Inaccessible Navigation

This navigation has several accessibility issues. Identify and fix them:

View the broken code
// โŒ BROKEN: Multiple accessibility issues
function BrokenNav() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="nav">
      <div onClick={() => setIsOpen(!isOpen)}>
        Menu โ˜ฐ
      </div>
      
      {isOpen && (
        <div className="menu">
          <div onClick={() => navigate('/')}>Home</div>
          <div onClick={() => navigate('/about')}>About</div>
          <div onClick={() => navigate('/contact')}>Contact</div>
        </div>
      )}
    </div>
  );
}
Hint: What's wrong?
  • No semantic HTML elements
  • No keyboard support
  • No ARIA attributes
  • No focus management
  • Using divs instead of buttons/links
Solution
// โœ… FIXED: Fully accessible navigation
function AccessibleNav() {
  const [isOpen, setIsOpen] = useState(false);
  const menuRef = useRef<HTMLElement>(null);
  
  const toggleMenu = () => {
    setIsOpen(!isOpen);
  };
  
  // Close on Escape
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && isOpen) {
        setIsOpen(false);
      }
    };
    
    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [isOpen]);
  
  return (
    <nav aria-label="Main navigation">
      <button
        onClick={toggleMenu}
        aria-expanded={isOpen}
        aria-controls="main-menu"
        aria-label="Toggle navigation menu"
      >
        Menu โ˜ฐ
      </button>
      
      {isOpen && (
        <ul id="main-menu" ref={menuRef}>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/contact">Contact</Link></li>
        </ul>
      )}
    </nav>
  );
}

๐ŸŽฏ Exercise 2: Create an Accessible Modal

Build a modal dialog from scratch with proper focus management and keyboard support.

Requirements
  • Focus modal when opened
  • Trap focus within modal
  • Close on Escape key
  • Restore focus when closed
  • Use proper ARIA attributes
  • Prevent body scroll when open
Hint: Start here
interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function Modal({ isOpen, onClose, title, children }: ModalProps) {
  // Your implementation here
  // Don't forget:
  // - useRef for modal element
  // - useRef for previous focus
  // - useEffect for focus management
  // - useEffect for Escape key
  // - useEffect for body scroll
  // - Focus trap in onKeyDown
  
  return (
    // Your JSX here
  );
}

๐ŸŽฏ Exercise 3: Audit an Existing Component

Use this checklist to audit one of your existing React components for accessibility:

Accessibility Audit Checklist

Semantic HTML

  • โ˜ Uses semantic elements (nav, main, header, etc.)
  • โ˜ Proper heading hierarchy (h1 โ†’ h2 โ†’ h3)
  • โ˜ Buttons for actions, links for navigation
  • โ˜ Form labels connected to inputs

Keyboard Navigation

  • โ˜ All interactive elements reachable by Tab
  • โ˜ Logical tab order
  • โ˜ Visible focus indicators
  • โ˜ No keyboard traps
  • โ˜ Escape closes modals/menus

ARIA

  • โ˜ ARIA used only when HTML isn't sufficient
  • โ˜ All interactive elements have accessible names
  • โ˜ aria-expanded for expandable elements
  • โ˜ aria-live for dynamic updates
  • โ˜ Proper roles for custom widgets

Visual

  • โ˜ Color contrast meets WCAG AA (4.5:1)
  • โ˜ Images have alt text
  • โ˜ Text can be resized to 200%
  • โ˜ No information conveyed by color alone

Forms

  • โ˜ Labels for all inputs
  • โ˜ Error messages announced to screen readers
  • โ˜ Required fields marked
  • โ˜ Instructions provided

๐Ÿ“š Summary

Congratulations! You've learned the fundamentals of web accessibility in React. Let's recap the key concepts:

๐ŸŽฏ Key Takeaways

โœ… What You've Learned

  • Why accessibility matters: Legal, ethical, and business benefits
  • WCAG guidelines: Four principles (POUR) and conformance levels
  • Semantic HTML: Using the right elements for meaning and structure
  • ARIA attributes: Roles, properties, and states for custom widgets
  • Keyboard navigation: Making all functionality keyboard accessible
  • Focus management: Controlling focus in SPAs and modals
  • Live regions: Announcing dynamic updates to screen readers

๐ŸŽจ The Accessibility Mindset

Building accessible applications isn't just about following rulesโ€”it's about adopting a mindset:

graph TD A[Plan for Accessibility] --> B[Design with Users in Mind] B --> C[Build Semantic HTML First] C --> D[Add ARIA When Needed] D --> E[Test with Real Users] E --> F[Iterate and Improve] F --> A style A fill:#667eea,stroke:#764ba2,stroke-width:2px,color:#fff style B fill:#48bb78,stroke:#38a169,stroke-width:2px,color:#fff style C fill:#ed8936,stroke:#dd6b20,stroke-width:2px,color:#fff style D fill:#4299e1,stroke:#3182ce,stroke-width:2px,color:#fff style E fill:#9f7aea,stroke:#805ad5,stroke-width:2px,color:#fff style F fill:#f687b3,stroke:#ed64a6,stroke-width:2px,color:#fff

๐Ÿ› ๏ธ Essential Tools

Keep these tools in your accessibility toolkit:

Tool Purpose Link
axe DevTools Automated accessibility testing deque.com/axe
WAVE Visual accessibility evaluation wave.webaim.org
Lighthouse Built into Chrome DevTools Chrome DevTools โ†’ Lighthouse
NVDA Free Windows screen reader nvaccess.org
VoiceOver Built-in macOS/iOS screen reader Built into Apple devices
Color Contrast Analyzer Check color contrast ratios tpgi.com/color-contrast-checker

๐Ÿ“– Quick Reference

Common ARIA Patterns Cheat Sheet
// Button
<button type="button">Click Me</button>

// Icon button
<button aria-label="Delete"><TrashIcon aria-hidden="true" /></button>

// Toggle button
<button aria-pressed={isPressed}>Bold</button>

// Expandable section
<button aria-expanded={isOpen} aria-controls="content-id">
  Section Title
</button>
<div id="content-id" hidden={!isOpen}>Content</div>

// Modal
<div role="dialog" aria-modal="true" aria-labelledby="title">
  <h2 id="title">Dialog Title</h2>
</div>

// Alert
<div role="alert">Error message</div>

// Status
<div role="status" aria-live="polite">Loading...</div>

// Tab interface
<div role="tablist">
  <button role="tab" aria-selected={isActive}>Tab 1</button>
</div>
<div role="tabpanel">Panel content</div>

๐Ÿ’ก Remember

Accessibility is not a featureโ€”it's a fundamental requirement. Every user deserves equal access to your application, regardless of their abilities. Build accessibility in from the start, test with real users, and keep learning!

๐Ÿš€ What's Next?

You've mastered accessibility fundamentals! Here's how to continue your journey:

๐Ÿ“š Further Learning

๐ŸŽฏ Immediate Action Items

๐Ÿ’ก This Week's Challenge

  1. Pick one of your React projects
  2. Run it through axe DevTools
  3. Fix the top 5 issues found
  4. Test with keyboard navigation only
  5. Try using a screen reader

๐ŸŒŸ Continue Building

Apply what you've learned in the next lesson where we'll deploy our accessible applications to production!