Skip to main content

JavaScript Accessibility

Introduction

Accessibility (often abbreviated as "a11y") is the practice of making your websites and web applications usable by as many people as possible, including those with disabilities. According to the World Health Organization, about 15% of the world's population lives with some form of disability. By implementing accessibility best practices in your JavaScript applications, you ensure that everyone can use your website, regardless of their abilities.

This guide will walk you through the fundamental concepts of JavaScript accessibility, provide practical examples, and demonstrate how to build inclusive web experiences.

Why Accessibility Matters

  • Ethical considerations: Everyone should have equal access to information and functionality on the web
  • Legal requirements: Many countries have laws requiring websites to be accessible (e.g., ADA in the US, EAA in the EU)
  • Business benefits: Accessible websites reach more users and improve SEO
  • Better UX for everyone: Accessibility improvements often enhance usability for all users

Core Accessibility Concepts for JavaScript Developers

1. Semantic HTML as a Foundation

Before writing JavaScript, ensure you're using proper semantic HTML. JavaScript can enhance accessibility, but it shouldn't replace good HTML structure.

jsx
// ❌ Poor accessibility
<div onclick="navigateToHome()">Home</div>

// ✅ Good accessibility
<a href="/" onclick="navigateToHome(event)">Home</a>

When using JavaScript frameworks like React, prefer semantic elements:

jsx
// ❌ Poor accessibility
const Header = () => (
<div className="header">
<div className="nav-item">Home</div>
</div>
);

// ✅ Good accessibility
const Header = () => (
<header>
<nav>
<a href="/">Home</a>
</nav>
</header>
);

2. Managing Focus

Focus management is critical for users who navigate with keyboards. When building interactive components or single-page applications, you need to ensure keyboard focus is properly managed.

Example: Custom Modal with Focus Management

jsx
function openModal() {
const modal = document.getElementById('my-modal');
const closeButton = modal.querySelector('.close-button');

// Show the modal
modal.style.display = 'block';

// Store the element that had focus before the modal was opened
const previouslyFocused = document.activeElement;

// Set focus to the close button
closeButton.focus();

// Trap focus inside the modal
modal.addEventListener('keydown', trapFocus);

// Close handler
function closeModal() {
modal.style.display = 'none';
modal.removeEventListener('keydown', trapFocus);
// Return focus to the element that had it before the modal opened
previouslyFocused.focus();
}

// Function to trap focus within the modal
function trapFocus(e) {
if (e.key === 'Escape') {
closeModal();
return;
}

if (e.key !== 'Tab') return;

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

// If shift+tab and on first element, go to last focusable element
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
// If tab and on last element, circle back to first element
else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}

// Set up the close button
closeButton.addEventListener('click', closeModal);
}

3. Working with ARIA (Accessible Rich Internet Applications)

ARIA attributes enhance HTML semantics for assistive technologies. Use them to communicate state changes and relationships that aren't expressed through standard HTML.

Example: Toggle Button with ARIA

jsx
function setupToggleButton() {
const button = document.getElementById('notification-toggle');

button.addEventListener('click', () => {
// Get the current state
const expanded = button.getAttribute('aria-pressed') === 'true';

// Toggle the state
button.setAttribute('aria-pressed', !expanded);

// Update the UI
button.innerText = expanded ? 'Enable Notifications' : 'Disable Notifications';

// Actual functionality
toggleNotifications(!expanded);
});
}

// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', setupToggleButton);

HTML:

html
<button 
id="notification-toggle"
aria-pressed="false"
class="toggle-button">
Enable Notifications
</button>

4. Handling Dynamic Content Updates

When JavaScript updates content dynamically, screen reader users might not be aware of the changes. Use ARIA live regions to announce important changes.

Example: Form Validation with Live Announcements

jsx
function validateForm() {
const nameInput = document.getElementById('name');
const errorContainer = document.getElementById('error-message');

if (nameInput.value.trim() === '') {
// Set the error message
errorContainer.textContent = 'Please enter your name';

// Make sure the container has the proper ARIA attributes
errorContainer.setAttribute('role', 'alert');
errorContainer.classList.add('error');

// Focus the input field for all users
nameInput.focus();

return false;
}

// Clear the error when valid
errorContainer.textContent = '';
errorContainer.removeAttribute('role');
errorContainer.classList.remove('error');

return true;
}

HTML:

html
<form onsubmit="return validateForm()">
<div class="form-group">
<label for="name">Your Name:</label>
<input type="text" id="name" />
<div id="error-message" class="error-text"></div>
</div>
<button type="submit">Submit</button>
</form>

5. Keyboard Accessibility

Ensure all interactive elements are accessible via keyboard. Custom components must handle keyboard events appropriately.

Example: Custom Dropdown Menu

jsx
function setupDropdown() {
const dropdown = document.getElementById('user-menu');
const button = dropdown.querySelector('.dropdown-toggle');
const menu = dropdown.querySelector('.dropdown-menu');
const items = menu.querySelectorAll('a');

// Toggle the dropdown with the button
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
toggleDropdown(!expanded);
});

// Close the dropdown when pressing escape
dropdown.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && button.getAttribute('aria-expanded') === 'true') {
toggleDropdown(false);
button.focus();
}
});

// Handle arrow key navigation within the dropdown
menu.addEventListener('keydown', (e) => {
const currentIndex = Array.from(items).indexOf(document.activeElement);

if (e.key === 'ArrowDown' && currentIndex < items.length - 1) {
items[currentIndex + 1].focus();
e.preventDefault();
} else if (e.key === 'ArrowUp' && currentIndex > 0) {
items[currentIndex - 1].focus();
e.preventDefault();
} else if (e.key === 'ArrowUp' && currentIndex === 0) {
button.focus();
e.preventDefault();
}
});

// Close the dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && button.getAttribute('aria-expanded') === 'true') {
toggleDropdown(false);
}
});

function toggleDropdown(open) {
button.setAttribute('aria-expanded', open);
menu.style.display = open ? 'block' : 'none';

// If opening, focus the first menu item
if (open && items.length) {
items[0].focus();
}
}
}

document.addEventListener('DOMContentLoaded', setupDropdown);

HTML:

html
<div id="user-menu" class="dropdown">
<button class="dropdown-toggle" aria-haspopup="true" aria-expanded="false">
User Menu
</button>
<ul class="dropdown-menu" style="display: none;">
<li><a href="/profile">Profile</a></li>
<li><a href="/settings">Settings</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</div>

Real-World Application: Accessible Tabs Component

Let's build a complete, accessible tabs component using JavaScript:

html
<div class="tabs" id="my-tabs">
<div role="tablist" aria-label="Programming Languages">
<button id="tab-js" role="tab" aria-selected="true" aria-controls="panel-js">JavaScript</button>
<button id="tab-py" role="tab" aria-selected="false" aria-controls="panel-py" tabindex="-1">Python</button>
<button id="tab-java" role="tab" aria-selected="false" aria-controls="panel-java" tabindex="-1">Java</button>
</div>

<div id="panel-js" role="tabpanel" aria-labelledby="tab-js">
<h3>JavaScript</h3>
<p>JavaScript is a programming language commonly used for web development.</p>
</div>

<div id="panel-py" role="tabpanel" aria-labelledby="tab-py" hidden>
<h3>Python</h3>
<p>Python is known for its readability and versatility.</p>
</div>

<div id="panel-java" role="tabpanel" aria-labelledby="tab-java" hidden>
<h3>Java</h3>
<p>Java is a class-based, object-oriented programming language.</p>
</div>
</div>
jsx
function setupTabs() {
const tablist = document.querySelector('[role="tablist"]');
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));

// Add keyboard support for the tabs
tablist.addEventListener('keydown', (e) => {
// Get the index of the current tab
const currentTabIndex = tabs.indexOf(document.activeElement);

// Define the direction based on which key is pressed
let nextTabIndex;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
nextTabIndex = (currentTabIndex + 1) % tabs.length;
e.preventDefault();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
nextTabIndex = (currentTabIndex - 1 + tabs.length) % tabs.length;
e.preventDefault();
} else if (e.key === 'Home') {
nextTabIndex = 0;
e.preventDefault();
} else if (e.key === 'End') {
nextTabIndex = tabs.length - 1;
e.preventDefault();
} else {
return; // Exit if not a navigation key
}

// Focus the next tab
tabs[nextTabIndex].focus();

// Activate the tab on arrow navigation (optional - depends on desired behavior)
activateTab(tabs[nextTabIndex]);
});

// Add click support for each tab
tabs.forEach(tab => {
tab.addEventListener('click', () => {
activateTab(tab);
});
});

function activateTab(tab) {
// Set the selected state on the tabs
tabs.forEach(t => {
const selected = t === tab;
t.setAttribute('aria-selected', selected);
t.setAttribute('tabindex', selected ? '0' : '-1');
});

// Show the corresponding panel and hide others
const panelId = tab.getAttribute('aria-controls');
const panels = Array.from(document.querySelectorAll('[role="tabpanel"]'));

panels.forEach(panel => {
const shouldShow = panel.id === panelId;
panel.hidden = !shouldShow;
});
}
}

document.addEventListener('DOMContentLoaded', setupTabs);

This tabs component includes:

  1. Proper ARIA roles, states, and properties
  2. Keyboard navigation between tabs
  3. Focus management for keyboard users
  4. Correctly associated tabs and panels
  5. Visible state changes

Testing Accessibility in JavaScript Applications

To ensure your JavaScript applications are accessible:

  1. Use automated testing tools:

    • Lighthouse in Chrome DevTools
    • axe DevTools browser extension
    • ESLint with eslint-plugin-jsx-a11y (for React projects)
  2. Keyboard testing:

    • Can you use all features without a mouse?
    • Is the focus order logical?
    • Are there keyboard traps?
  3. Screen reader testing:

    • Test with at least one screen reader: NVDA or JAWS (Windows), VoiceOver (macOS/iOS), TalkBack (Android)
    • Verify announcements are clear and meaningful
  4. Reduced motion considerations:

    jsx
    function setupAnimation() {
    // Check if the user prefers reduced motion
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    const element = document.querySelector('.animated-element');

    if (prefersReducedMotion) {
    // Apply subtle or no animation
    element.classList.add('reduced-motion');
    } else {
    // Apply standard animation
    element.classList.add('full-animation');
    }

    // Listen for preference changes
    window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', event => {
    if (event.matches) {
    element.classList.replace('full-animation', 'reduced-motion');
    } else {
    element.classList.replace('reduced-motion', 'full-animation');
    }
    });
    }

Summary

Implementing accessibility in JavaScript is about:

  1. Building on a foundation of semantic HTML
  2. Managing keyboard focus effectively
  3. Using ARIA attributes appropriately to enhance semantics
  4. Ensuring dynamic content changes are announced to screen readers
  5. Supporting keyboard interaction for all interactive elements
  6. Testing with assistive technologies

By following these accessibility best practices, you'll create JavaScript applications that are more inclusive, compliant with legal standards, and often more usable for everyone.

Additional Resources

Practice Exercises

  1. Take an existing button on a webpage and make it toggle between two states, ensuring it's accessible using ARIA attributes.
  2. Create an accessible autocomplete component that can be used with keyboard-only navigation.
  3. Build a custom modal dialog that manages focus correctly and allows closing with the Escape key.
  4. Add appropriate ARIA live regions to a form that shows validation errors dynamically.
  5. Audit a JavaScript component you've built previously and improve its accessibility based on what you've learned.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)