30 Days of JavaScript: Accessibility and JavaScript, Day 20

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: Accessibility and JavaScript, Day 20

Enhance web accessibility with JavaScript. Explore ARIA attributes, accessible patterns, and keyboard navigation to create inclusive, functional web apps.

Tab through enough of the web and the cracks show fast. A menu that won’t open from the keyboard. A button a screen reader names but never fires. A form that flags an error only sighted people ever see. That’s not a mouse problem or an eyesight problem. It’s a build problem.

Day 20 of the 30 Days of JavaScript series is about accessibility: where JavaScript helps, and where it quietly hurts. We’ll go semantic HTML first, ARIA when you actually need it, then the JS patterns for keyboard, focus, forms, and live updates. The goal is an interface everyone can drive.

Table of Contents

Understanding Web Accessibility

What Accessibility Actually Means

Accessibility means people can use what you built whatever their ability. Someone on a screen reader, tabbing with a keyboard, or using voice control should reach the same place a mouse user does. If your interface only works with a mouse and working eyes, you’ve quietly locked people out.

Why It Matters

There’s a legal floor in a lot of places, the Americans with Disabilities Act (ADA) in the US and similar rules elsewhere, and that’s real. But the better reason is simpler: accessible interfaces are clearer for everyone. Honest labels, sane keyboard order, hit targets you can actually hit. Nobody complains a site is too easy to use.

Web Content Accessibility Guidelines (WCAG)

The Web Content Accessibility Guidelines (WCAG) are the reference standard. Four principles, easy to hold in your head as POUR: Perceivable, Operable, Understandable, and Robust. You don’t need to memorize the spec to start. You keep it in the back of your mind while you build.

HTML
/**
 * Example of using the alt attribute for image accessibility.
 */
<img src="example.jpg" alt="A description of the image" />

The alt attribute is the smallest example and the one people still skip. A screen reader reads that text in place of the image. One caveat the snippet doesn’t show: if an image is purely decorative, give it an empty alt="" so assistive tech skips it instead of reading out a filename.

ARIA Attributes and Semantic HTML

What ARIA Is For

ARIA (Accessible Rich Internet Applications) is a set of attributes that tell assistive tech about roles, states, and properties plain HTML can’t express on its own. It’s useful, and it’s easy to misuse. Bad ARIA is worse than none: surveys keep finding pages with ARIA carry more detected errors than pages without it, because people reach for it before reaching for the right element.

Using ARIA Roles

A role tells assistive tech what a thing is: a button, a navigation landmark, an alert. Here’s a close button labeled for screen readers:

HTML
/**
 * Example of adding ARIA roles and labels to enhance accessibility.
 */
<button aria-label="Close" role="button">X</button>

The aria-label earns its place: when the visible text is just an X, it gives a screen reader something to read. But that snippet makes a common mistake. role="button" on a real <button> is redundant, since a native button already carries the button role. The first rule of ARIA is not to use ARIA when a plain element already does the job. Keep the aria-label, drop the role.

ARIA States and Properties

States and properties carry the live detail. aria-expanded tells a screen reader whether a collapsible thing is open or shut, and you flip it in JavaScript when the state changes.

HTML
/**
 * Example of using ARIA attributes for dynamic elements.
 */
<div role="button" aria-expanded="false" onclick="toggleMenu()">Menu</div>

Read this one with a warning. A <div role="button"> is not a button until you finish the job. It needs tabindex="0" so keyboard users can reach it, and a keydown handler for Enter and Space, because a div won’t fire on those keys the way a real button does. onclick alone leaves keyboard users stuck. The honest move: use a real <button> and skip all of it. Reach for role="button" on a div only when you truly can’t.

Semantic HTML First

This is the throughline. Semantic elements like <nav>, <header>, <main>, <article>, and <button> ship with roles, keyboard behavior, and meaning already wired in, and assistive tech reads them for free. Start there, and reach for ARIA only to fill the gaps semantics can’t cover.

Accessible JavaScript Patterns

Keyboard Navigation

Everything you can do with a mouse has to work with a keyboard: managing focus, listening for the right keys, and making sure every interactive thing is reachable by Tab. Here’s the keyboard handler for a custom control:

JS
/**
 * Example of adding keyboard support to interactive elements.
 *
 * @param {KeyboardEvent} event - The keyboard event to listen for.
 * @returns {void}
 */
document.addEventListener('keydown', function(event) {
  if (event.key === 'Enter' || event.key === ' ') {
    // Trigger button click or toggle action
  }
});

Two things. event.key === ' ' is the space bar, matched as a single-space string, and Enter is 'Enter'. And you only need this for custom widgets. Native <button> and <a> already fire on these keys, which is one more reason to use them.

Managing Focus

Focus is the keyboard user’s cursor. When a modal opens or content swaps in over AJAX, move focus somewhere sensible, or people get dumped back at the top of the page with no idea what changed.

JS
/**
 * Opens a modal and moves focus to the close button.
 *
 * @returns {void}
 */
function openModal() {
  document.getElementById('modal').style.display = 'block';
  document.getElementById('close-button').focus(); // Moves focus to the close button
}

This drops focus on the close button the moment the modal opens, so a keyboard user can act right away. Pair it with sending focus back to the element that opened the modal on close, so they land where they left off.

Accessible Forms with JavaScript

Forms are where accessibility quietly fails. A red border means nothing to someone who can’t see it. Use JavaScript to mark the state and put the problem into text.

JS
/**
 * Validates form input and provides accessible error feedback.
 *
 * @returns {boolean} - Returns true if the form is valid.
 */
function validateForm() {
  const nameField = document.getElementById('name');
  if (!nameField.value) {
    nameField.setAttribute('aria-invalid', 'true');
    document.getElementById('name-error').textContent = 'Name is required';
    nameField.focus(); // Focuses the user’s attention on the error
    return false;
  }
  return true;
}

This marks the field invalid with aria-invalid and writes an error into a message element, which is the right start. Finish it by linking the two: add aria-describedby="name-error" to the input so a screen reader reads the error as part of the field. aria-invalid flags that something’s wrong; aria-describedby says what. You want both, plus the focus move the snippet already does.

ARIA Live Regions for Dynamic Content

Some updates happen with no page load: a cart count ticks up, a save succeeds, a search returns. A live region lets you announce those to a screen reader without stealing focus.

HTML
/**
 * Example of using ARIA live regions for dynamic updates.
 */
<div aria-live="polite" id="status-message"></div>
<script>
/**
 * Updates the status message and announces it to screen readers.
 *
 * @param {string} message - The status message to display.
 * @returns {void}
 */
function updateStatus(message) {
  document.getElementById('status-message').textContent = message;
}
</script>

aria-live="polite" waits for a natural pause before announcing, which is right for status messages. Save aria-live="assertive" for the rare update that has to interrupt, like a hard error. One detail: set the region up empty in the HTML first, then write into it, so it’s the change that gets announced.

Accessible Modal Dialogs

A modal has to keep keyboard focus inside it while it’s open, or Tab wanders off behind the overlay into content nobody can see. The classic fix traps focus by hand:

JS
/**
 * Traps focus within a modal, ensuring keyboard users cannot navigate out of it.
 *
 * @param {HTMLElement} modal - The modal element to trap focus in.
 * @returns {void}
 */
function trapFocus(modal) {
  const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];
  modal.addEventListener('keydown', function(event) {
    if (event.key === 'Tab') {
      if (event.shiftKey) { // Backwards Tab
        if (document.activeElement === firstElement) {
          event.preventDefault();
          lastElement.focus();
        }
      } else { // Forward Tab
        if (document.activeElement === lastElement) {
          event.preventDefault();
          firstElement.focus();
        }
      }
    }
  });
}
trapFocus(document.getElementById('modal'));

That works, and for years it was the job. But there’s a better answer now: the native <dialog> element. Call dialog.showModal() and the browser traps focus for you, closes on Escape, and makes the rest of the page inert so assistive tech ignores it. Same first-rule logic: the platform grew the feature, so lean on it. Keep trapFocus for older targets or custom overlays, but reach for <dialog> first.

Conclusion

Accessibility isn’t a checklist you clear once. It’s a habit: semantic HTML first, ARIA to fill the real gaps, JavaScript to manage focus and announce what changed. None of today’s pieces are hard on their own. The work is remembering to do them while you build instead of after.

And there’s a payoff nobody warns you about. Get this right and you don’t just help people using assistive tech, you get cleaner markup and clearer state, code that’s easier to reason about six months later. The accessible version is usually the better-built version. Bolting it on at the end always costs more.

What’s Next?

In Day 21 we move off the browser and into Node.js, taking JavaScript to the server. See you there.

Next: 30 Days of JavaScript: Introduction to Node.js, Day 21

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top