article Part 5 of 6

Understanding Screen Readers

A screen reader is software that reads digital text aloud and converts web content into speech or braille. For blind or low-vision users, it's their primary way to interact with websites.

Screen readers do more than just read text—they interpret the structure and semantics of your HTML to help users navigate:

  • Announce headings and let users jump between them
  • List all links on a page
  • Navigate by landmarks (header, nav, main, footer)
  • Announce form labels and input types
  • Read ARIA attributes to understand dynamic content

This is why semantic HTML and proper ARIA usage are so important—they provide the information screen readers need to make sense of your content.

Common Screen Readers

  • JAWS (Job Access With Speech) – Popular commercial screen reader for Windows
  • NVDA (NonVisual Desktop Access) – Free, open-source screen reader for Windows
  • VoiceOver – Built into macOS and iOS
  • TalkBack – Built into Android
  • Narrator – Built into Windows

You don't need to test with all of them, but testing with at least one (like NVDA or VoiceOver since they're free/built-in) is invaluable.

How Screen Readers Navigate

Screen reader users don't typically listen to a page from top to bottom. They navigate efficiently using:

Keyboard Shortcuts

  • H key – Jump to next heading (1-6 for specific levels)
  • Tab – Move through interactive elements
  • Arrow keys – Read line by line or word by word
  • R key – Jump to regions/landmarks
  • D key – Jump to landmarks
  • F key – Jump to form controls
  • T key – Jump to tables

This is why proper semantic structure is crucial. If you use <div class="heading"> instead of <h2>, screen reader users can't jump to it.

Rotor/Elements List

Screen readers can generate lists of elements:

  • All headings on the page
  • All links
  • All form controls
  • All landmarks

Users can scan these lists and jump directly to what they need. This makes descriptive headings and link text essential—a list of 20 "Click here" links is useless.

Making Dynamic Content Accessible

Modern websites update content without full page reloads. Screen readers need to know about these changes.

Status Messages (WCAG 4.1.3)

When content updates dynamically (like form submission results, loading indicators, or notification messages), screen readers might not notice unless you tell them.

ARIA Live Regions solve this:

role="status" or aria-live="polite"

For non-critical updates that can wait until the user pauses:

<div role="status" aria-live="polite">
    5 items added to cart
</div>

<div id="search-results" aria-live="polite">
    Showing 23 results for "accessibility"
</div>

role="alert" or aria-live="assertive"

For important messages that should interrupt immediately:

<div role="alert">
    Error: Password must be at least 8 characters
</div>

<div aria-live="assertive">
    Connection lost. Attempting to reconnect...
</div>

Best Practices for Live Regions

  • The container should exist in the DOM when the page loads (even if empty)
  • Update the content with JavaScript—don't create and insert a new live region
  • Use polite for most things; reserve assertive for truly critical messages
  • Keep messages concise and clear
  • Don't overuse—too many announcements overwhelm users
<!-- In your HTML -->
<div id="status" role="status" aria-live="polite" aria-atomic="true"></div>

<!-- In your JavaScript -->
const statusEl = document.getElementById('status');
statusEl.textContent = 'Form submitted successfully';

// Clear after a few seconds
setTimeout(() => {
    statusEl.textContent = '';
}, 5000);

Accessible Modal Dialogs

Modal dialogs are one of the most common interactive patterns, and they need careful handling for accessibility.

Requirements for Accessible Modals

  1. Focus Management: Move focus into the modal when it opens
  2. Focus Trap: Keep focus within the modal (tab loops through modal elements)
  3. Escape to Close: Allow Escape key to close the modal
  4. Return Focus: Return focus to the triggering element when closed
  5. Inert Background: Make background content non-interactive
  6. ARIA Attributes: Use proper roles and properties

Modal Implementation

<button id="open-modal">Open Dialog</button>

<div
    id="modal"
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    aria-describedby="modal-desc"
    hidden>

    <h2 id="modal-title">Confirm Action</h2>
    <p id="modal-desc">Are you sure you want to delete this item?</p>

    <button id="confirm">Confirm</button>
    <button id="cancel">Cancel</button>
    <button id="close-modal" aria-label="Close">×</button>
</div>

<script>
const modal = document.getElementById('modal');
const openBtn = document.getElementById('open-modal');
const closeBtn = document.getElementById('close-modal');
const cancelBtn = document.getElementById('cancel');

let previousFocus;

function openModal() {
    // Save current focus
    previousFocus = document.activeElement;

    // Show modal
    modal.removeAttribute('hidden');

    // Add background inertness (make background non-interactive)
    document.body.setAttribute('aria-hidden', 'true');
    modal.removeAttribute('aria-hidden');

    // Move focus to first focusable element (or close button)
    closeBtn.focus();

    // Add event listeners
    modal.addEventListener('keydown', handleModalKeydown);
}

function closeModal() {
    // Hide modal
    modal.setAttribute('hidden', '');

    // Remove background inertness
    document.body.removeAttribute('aria-hidden');

    // Return focus
    if (previousFocus) {
        previousFocus.focus();
    }

    // Remove event listeners
    modal.removeEventListener('keydown', handleModalKeydown);
}

function handleModalKeydown(e) {
    // Close on Escape
    if (e.key === 'Escape') {
        closeModal();
        return;
    }

    // Trap focus on Tab
    if (e.key === 'Tab') {
        const focusableElements = modal.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];

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

openBtn.addEventListener('click', openModal);
closeBtn.addEventListener('click', closeModal);
cancelBtn.addEventListener('click', closeModal);
</script>

Common Interactive Patterns

Accordion (Expand/Collapse)

<div class="accordion">
    <h3>
        <button
            aria-expanded="false"
            aria-controls="section1-content"
            id="section1-header">
            Section 1
        </button>
    </h3>
    <div id="section1-content" hidden>
        <p>Content for section 1...</p>
    </div>
</div>

<script>
const button = document.getElementById('section1-header');
const content = document.getElementById('section1-content');

button.addEventListener('click', () => {
    const isExpanded = button.getAttribute('aria-expanded') === 'true';

    button.setAttribute('aria-expanded', !isExpanded);
    content.hidden = isExpanded;
});
</script>

Key points:

  • Use <button> for the header (not div or h3 directly)
  • Update aria-expanded when state changes
  • Use hidden attribute or display: none for collapsed content

Tabs

<div class="tabs">
    <div role="tablist" aria-label="Sample Tabs">
        <button role="tab" aria-selected="true" aria-controls="panel1" id="tab1">
            Tab 1
        </button>
        <button role="tab" aria-selected="false" aria-controls="panel2" id="tab2">
            Tab 2
        </button>
    </div>

    <div role="tabpanel" id="panel1" aria-labelledby="tab1">
        <p>Content for tab 1</p>
    </div>

    <div role="tabpanel" id="panel2" aria-labelledby="tab2" hidden>
        <p>Content for tab 2</p>
    </div>
</div>

Keyboard behavior:

  • Arrow keys navigate between tabs
  • Enter/Space activates a tab
  • Only the active tab is in tab order (use tabindex="-1" on inactive tabs)

Carousels

Accessibility requirements:

  • Provide Previous/Next buttons (keyboard accessible)
  • Add pause/play controls (WCAG 2.2.2)
  • Use aria-label on the container ("Image carousel")
  • Announce slide changes with aria-live="polite"
  • Make slide indicators keyboard accessible
  • Don't auto-advance too quickly (or at all without user control)

Testing with Screen Readers

Getting Started with NVDA (Windows)

  1. Download free from nvaccess.org
  2. Install and restart
  3. Launch NVDA (it starts talking immediately)
  4. Open your website in a browser
  5. Use arrow keys to read through content
  6. Try Insert + F7 to see elements list
  7. Try H to jump through headings

Getting Started with VoiceOver (Mac)

  1. Press Cmd + F5 to toggle VoiceOver
  2. Open your website in Safari (works best with VoiceOver)
  3. Use VO (Control + Option) + arrow keys to navigate
  4. Use VO + U to open rotor (elements list)
  5. Use VO + Cmd + H to jump through headings

What to Listen For

  • Does everything get announced? Text, headings, links, buttons
  • Are controls identified properly? "Button: Submit" not "Clickable: Submit"
  • Are labels meaningful? "Button: Delete item" not "Button: Icon"
  • Does state get announced? "Expanded" vs "Collapsed", "Checked" vs "Not checked"
  • Is the reading order logical? Does it match visual order?
  • Are images described? Alt text should be read
  • Are updates announced? Dynamic content changes

Key Takeaways

  • Screen readers rely on semantic HTML and ARIA to understand and navigate content.
  • Users navigate by headings, landmarks, links, and form controls—use proper elements.
  • Use ARIA live regions (role="status", role="alert") to announce dynamic updates.
  • Modal dialogs need focus management, focus trapping, and proper ARIA attributes.
  • Interactive widgets (accordions, tabs, carousels) need both keyboard support and ARIA semantics.
  • Test with an actual screen reader—NVDA (Windows) or VoiceOver (Mac) are free.
  • Listen for proper announcements, clear labels, and logical reading order.
  • Update aria-expanded, aria-selected, and other states when content changes.

Now that you understand how to build accessible content, let's look at how to test and audit your site for accessibility issues—both automated and manual testing approaches.