ACCESSIBILITY

Overview

Accessibility

This guide covers accessibility best practices for implementing overlays in Spectrum Web Components.

Table of contents

Section titled Table of contents
  • Focus management
  • ARIA patterns
  • Keyboard navigation
  • Screen reader support
  • Color contrast and visual indicators
  • Common accessibility issues

Focus management

Section titled Focus management

Proper focus management is critical for keyboard users and screen reader users to interact with overlays effectively.

Focus behavior by overlay type

Section titled Focus behavior by overlay type

Different overlay types have different focus management behaviors:

modal and page overlays:

  • Always trap focus within the overlay
  • Focus moves to first focusable element (or overlay container if none)
  • Cannot tab outside the overlay
  • ESC key behavior: modal cannot close with ESC, page can close with ESC
<sp-overlay trigger="open-dialog@click" type="modal" receives-focus="true">
    <sp-popover>
        <sp-dialog>
            <h2 slot="heading">Confirm Action</h2>
            <p>Are you sure you want to proceed?</p>
            <sp-button slot="button" variant="accent">Confirm</sp-button>
            <sp-button slot="button" variant="secondary">Cancel</sp-button>
        </sp-dialog>
    </sp-popover>
</sp-overlay>

auto overlays:

  • Accept focus into the overlay content
  • Do not trap focus (user can tab outside)
  • Close automatically when focus moves outside
  • ESC key closes the overlay
<sp-overlay trigger="menu-btn@click" type="auto" receives-focus="true">
    <sp-popover>
        <sp-menu>
            <sp-menu-item>Option 1</sp-menu-item>
            <sp-menu-item>Option 2</sp-menu-item>
        </sp-menu>
    </sp-popover>
</sp-overlay>

hint overlays:

  • No focus management (non-interactive)
  • Close on any user interaction
  • Best for tooltips that provide supplementary information
<sp-overlay trigger="help-btn@hover" type="hint" delayed placement="top">
    <sp-tooltip>This is helpful information</sp-tooltip>
</sp-overlay>

manual overlays:

  • Accept focus into the overlay content
  • Do not trap focus
  • Only close on ESC key or programmatic close
  • Best for persistent information popovers

Controlling focus with receives-focus

Section titled Controlling focus with receives-focus

Use the receives-focus attribute to control whether the overlay receives focus when opened:

<!-- Always move focus (good for dialogs and menus) -->
<sp-overlay receives-focus="true" type="modal">
    <sp-popover>
        <sp-dialog>...</sp-dialog>
    </sp-popover>
</sp-overlay>

<!-- Never move focus (good for non-interactive hints) -->
<sp-overlay receives-focus="false" type="auto">
    <sp-popover>
        <p>Informational content</p>
    </sp-popover>
</sp-overlay>

<!-- Automatic based on type (default) -->
<sp-overlay receives-focus="auto" type="auto">
    <sp-popover>...</sp-popover>
</sp-overlay>

Returning focus to trigger

Section titled Returning focus to trigger

When an overlay closes, focus should return to the trigger element:

const overlay = document.querySelector('sp-overlay');

// Focus automatically returns to trigger when overlay closes
overlay.addEventListener('sp-closed', () => {
    console.log('Focus returned to trigger element');
});

This is handled automatically by the overlay system for most cases.

ARIA patterns

Section titled ARIA patterns

Required ARIA attributes

Section titled Required ARIA attributes

Trigger elements:

<!-- For menu triggers -->
<sp-button
    id="menu-trigger"
    aria-haspopup="menu"
    aria-expanded="false"
    aria-controls="menu-overlay"
>
    Actions
</sp-button>

<!-- For dialog triggers -->
<sp-button id="dialog-trigger" aria-haspopup="dialog" aria-expanded="false">
    Open Dialog
</sp-button>

Overlay content:

<!-- Dialog overlay -->
<sp-overlay type="modal">
    <sp-popover role="dialog" aria-modal="true" aria-labelledby="dialog-title">
        <sp-dialog>
            <h2 id="dialog-title" slot="heading">Dialog Title</h2>
            <p>Dialog content...</p>
        </sp-dialog>
    </sp-popover>
</sp-overlay>

<!-- Menu overlay -->
<sp-overlay type="auto">
    <sp-popover role="menu" aria-label="Actions menu">
        <sp-menu>
            <sp-menu-item role="menuitem">Copy</sp-menu-item>
            <sp-menu-item role="menuitem">Paste</sp-menu-item>
        </sp-menu>
    </sp-popover>
</sp-overlay>

Descriptive relationships

Section titled Descriptive relationships

Use aria-describedby to associate tooltip content with trigger elements:

<sp-button id="save-btn" aria-describedby="save-tooltip">Save</sp-button>

<sp-overlay trigger="save-btn@hover" type="hint">
    <sp-tooltip id="save-tooltip">Save your changes (Ctrl+S)</sp-tooltip>
</sp-overlay>

This relationship is automatically managed by HoverController and LongpressController.

Live regions for dynamic content

Section titled Live regions for dynamic content

For overlays with dynamic content that should be announced:

<sp-overlay type="auto">
    <sp-popover>
        <div aria-live="polite" aria-atomic="true">
            <p id="status-message">Loading...</p>
        </div>
    </sp-popover>
</sp-overlay>

Keyboard navigation

Section titled Keyboard navigation

Standard keyboard interactions

Section titled Standard keyboard interactions

All overlays:

  • ESC - Close overlay (except modal type)
  • TAB - Move focus forward (within overlay for modal, or to next element for non-modal)
  • Shift+TAB - Move focus backward

Menu overlays:

  • Arrow Up/Down - Navigate menu items
  • Home - First menu item
  • End - Last menu item
  • Enter/Space - Activate menu item

Dialog overlays:

  • TAB - Cycle through interactive elements
  • Shift+TAB - Cycle backward
  • Focus trapped within dialog for modal type

Longpress overlays:

  • Space - Trigger longpress overlay
  • Alt+Down Arrow - Trigger longpress overlay

Implementing custom keyboard handlers

Section titled Implementing custom keyboard handlers

For custom keyboard interactions:

overlay.addEventListener('keydown', (event) => {
    // Custom keyboard handling
    if (event.key === 'Enter' && event.ctrlKey) {
        event.preventDefault();
        // Perform custom action
        overlay.open = false;
    }
});

Ensuring keyboard-only users can access hover content

Section titled Ensuring keyboard-only users can access hover content

Hover tooltips should also be accessible via keyboard focus:

<overlay-trigger>
    <sp-button slot="trigger">Help</sp-button>

    <!-- This tooltip appears on BOTH hover and keyboard focus -->
    <sp-tooltip slot="hover-content">
        Helpful information accessible to keyboard users
    </sp-tooltip>
</overlay-trigger>

Screen reader support

Section titled Screen reader support

Announcing overlay state changes

Section titled Announcing overlay state changes

The overlay system dispatches events that can be used to announce state changes:

overlay.addEventListener('sp-opened', () => {
    // Optionally announce that overlay opened
    // Most screen readers will announce this automatically via role/aria-modal
});

overlay.addEventListener('sp-closed', () => {
    // Optionally announce that overlay closed
});

Proper heading structure

Section titled Proper heading structure

Maintain logical heading hierarchy within overlay content:

<sp-overlay type="modal">
    <sp-popover>
        <sp-dialog aria-labelledby="main-heading">
            <h2 id="main-heading" slot="heading">Main Dialog Title</h2>

            <!-- Subsection within dialog -->
            <h3>Subsection Title</h3>
            <p>Subsection content...</p>

            <!-- Another subsection -->
            <h3>Another Subsection</h3>
            <p>More content...</p>
        </sp-dialog>
    </sp-popover>
</sp-overlay>

Descriptive labels for all interactive elements

Section titled Descriptive labels for all interactive elements
<sp-overlay type="auto">
    <sp-popover>
        <sp-menu>
            <!-- Good: Descriptive text -->
            <sp-menu-item>Copy to clipboard</sp-menu-item>
            <sp-menu-item>Delete permanently</sp-menu-item>

            <!-- Avoid: Vague text -->
            <!-- <sp-menu-item>Copy</sp-menu-item> -->
            <!-- <sp-menu-item>Delete</sp-menu-item> -->
        </sp-menu>
    </sp-popover>
</sp-overlay>

Error messages and validation

Section titled Error messages and validation

Ensure error messages in overlays are announced:

<sp-overlay type="modal" receives-focus="true">
    <sp-popover>
        <sp-dialog>
            <h2 slot="heading">Edit Profile</h2>

            <sp-field-label for="username">Username</sp-field-label>
            <sp-textfield
                id="username"
                aria-describedby="username-error"
                aria-invalid="true"
            ></sp-textfield>

            <!-- Error message with role="alert" for announcement -->
            <sp-help-text id="username-error" variant="negative" role="alert">
                Username must be at least 3 characters
            </sp-help-text>
        </sp-dialog>
    </sp-popover>
</sp-overlay>

Color contrast and visual indicators

Section titled Color contrast and visual indicators

Focus indicators

Section titled Focus indicators

Ensure focus indicators meet WCAG AA standards (3:1 contrast ratio):

/* Custom focus indicator for overlay content */
sp-overlay sp-button:focus-visible {
    outline: 2px solid var(--spectrum-blue-800);
    outline-offset: 2px;
}

Visual distinction from page content

Section titled Visual distinction from page content

Modal overlays should have a backdrop that clearly distinguishes them from the page:

<!-- Modal with backdrop (automatic) -->
<sp-overlay type="modal">
    <sp-popover>
        <sp-dialog>...</sp-dialog>
    </sp-popover>
</sp-overlay>

High contrast mode support

Section titled High contrast mode support

Test overlays in high contrast mode to ensure visibility:

@media (prefers-contrast: high) {
    sp-overlay[type='modal'] {
        border: 2px solid currentColor;
    }
}

Common accessibility issues

Section titled Common accessibility issues

Issue: Trigger element is not keyboard accessible

Section titled Issue: Trigger element is not keyboard accessible

Problem:

<!-- BAD: div is not keyboard accessible -->
<div id="menu-trigger" onclick="openMenu()">Menu</div>
<sp-overlay trigger="menu-trigger@click" type="auto">
    <sp-popover>...</sp-popover>
</sp-overlay>

Solution:

<!-- GOOD: Use button or other interactive element -->
<sp-button id="menu-trigger">Menu</sp-button>
<sp-overlay trigger="menu-trigger@click" type="auto">
    <sp-popover>...</sp-popover>
</sp-overlay>

Issue: Hover content contains interactive elements

Section titled Issue: Hover content contains interactive elements

Problem:

<!-- BAD: hover-content should be non-interactive -->
<overlay-trigger>
    <sp-button slot="trigger">Help</sp-button>
    <sp-popover slot="hover-content">
        <sp-link href="/help">Read more</sp-link>
    </sp-popover>
</overlay-trigger>

Solution:

<!-- GOOD: Use click-content for interactive overlays -->
<overlay-trigger>
    <sp-button slot="trigger">Help</sp-button>
    <sp-tooltip slot="hover-content">Click for more information</sp-tooltip>
    <sp-popover slot="click-content">
        <sp-dialog>
            <h2 slot="heading">Help</h2>
            <p>Detailed help content...</p>
            <sp-link href="/help">Read full documentation</sp-link>
        </sp-dialog>
    </sp-popover>
</overlay-trigger>

Issue: Missing ARIA attributes

Section titled Issue: Missing ARIA attributes

Problem:

<!-- BAD: No aria-haspopup or aria-expanded -->
<sp-button id="dialog-trigger">Open</sp-button>

Solution:

<!-- GOOD: Proper ARIA attributes -->
<sp-button id="dialog-trigger" aria-haspopup="dialog" aria-expanded="false">
    Open
</sp-button>

Issue: Focus not returned to trigger

Section titled Issue: Focus not returned to trigger

This is usually handled automatically, but if you're managing overlay lifecycle manually:

const overlay = await openOverlay(content, options);
const triggerElement = document.querySelector('#trigger');

overlay.addEventListener(
    'sp-closed',
    () => {
        // Return focus to trigger
        triggerElement.focus();
        overlay.remove();
    },
    { once: true }
);

Issue: Insufficient color contrast

Section titled Issue: Insufficient color contrast

Test with browser DevTools or accessibility tools to ensure:

  • Text has at least 4.5:1 contrast ratio (WCAG AA)
  • Large text (18pt+) has at least 3:1 contrast ratio
  • Focus indicators have at least 3:1 contrast ratio

Testing accessibility

Section titled Testing accessibility

Automated testing

Section titled Automated testing

Use tools like:

  • axe DevTools - Browser extension for accessibility testing
  • WAVE - Web accessibility evaluation tool
  • Lighthouse - Built into Chrome DevTools

Manual testing checklist

Section titled Manual testing checklist
  • [ ] Can you reach all interactive elements with keyboard only?
  • [ ] Is focus visible at all times?
  • [ ] Does ESC key close the overlay?
  • [ ] Is focus returned to trigger when overlay closes?
  • [ ] Are all interactive elements properly labeled?
  • [ ] Can screen readers access and understand all content?
  • [ ] Is the overlay announced when it opens?
  • [ ] Does the overlay work in high contrast mode?
  • [ ] Is color not the only means of conveying information?

Screen reader testing

Section titled Screen reader testing

Test with at least one screen reader:

  • NVDA (Windows, free)
  • JAWS (Windows, commercial)
  • VoiceOver (macOS/iOS, built-in)
  • TalkBack (Android, built-in)

Resources

Section titled Resources
  • W3C ARIA Authoring Practices
  • WebAIM Keyboard Accessibility
  • WCAG 2.1 Guidelines
  • MDN Accessibility

See also

Section titled See also
  • ARCHITECTURE.md - Overlay system architecture
  • TROUBLESHOOTING.md - Common issues and solutions
  • overlay-trigger.md - Multi-interaction overlays