Skip to content

JavaScript Tutorial Accessibility with JavaScript

Accessible Interactive Widgets

Lesson 221 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Tabs, accordions, menus and carousels are where accessibility problems pile up. Here is a short, practical approach.

Step 1: can native HTML do it?

WidgetNative option
Show/hide section<details> / <summary>
Popup<dialog>
Choose one of many<select>, radio buttons
Slider<input type="range">
Progress<progress>, <meter>
Autocomplete suggestions<datalist>

If one fits, use it, and style it with CSS.

Step 2: the checklist for anything you build

  1. Use real elements (button for actions, a for navigation).
  2. Keyboard works: Tab reaches it, Enter and Space activate, Escape closes, arrows move inside groups.
  3. Visible focus at all times.
  4. State is announced: aria-expanded, aria-selected, aria-pressed, aria-current, kept in sync with what you see.
  5. A name for everything: visible text, aria-label, or aria-labelledby.
  6. Do not hide content that must be available by only opacity: 0. Use hidden or display: none for what should be unreachable.
  7. Respect user preferences: prefers-reduced-motion for animation.
  8. Manage focus when content opens, closes or changes (Focus Management).

An accessible accordion in a few lines

Buttons control panels, and the state is exposed with ARIA.

index.html
HTML
<style>
  .acc button { display: block; width: 100%; text-align: left; padding: 8px; background: #f4f9fc; border: 1px solid #1a5276; }
  .acc button:focus-visible { outline: 3px solid #d94a00; }
  .acc [role="region"] { padding: 8px; border: 1px solid #ddd; border-top: 0; }
</style>
<div class="acc" id="acc">
  <h3><button aria-expanded="false" aria-controls="p1" id="b1">What is JavaScript?</button></h3>
  <div role="region" id="p1" aria-labelledby="b1" hidden>The language of the web.</div>
  <h3><button aria-expanded="false" aria-controls="p2" id="b2">Is it hard?</button></h3>
  <div role="region" id="p2" aria-labelledby="b2" hidden>Not with practice.</div>
</div>
<script>
  document.querySelector("#acc").addEventListener("click", event => {
    const button = event.target.closest("button[aria-controls]");
    if (!button) return;
    const open = button.getAttribute("aria-expanded") === "true";
    button.setAttribute("aria-expanded", String(!open));
    document.getElementById(button.getAttribute("aria-controls")).hidden = open;
  });
</script>

What makes it accessible:

  • The headers are real <button> elements inside headings, so they are focusable and activated with Enter and Space with no extra code.
  • aria-expanded announces the state, aria-controls links to the panel.
  • Panels use hidden, so closed content is not reachable.

Tabs: an arrow-key pattern

Tabs use role="tablist", role="tab" with aria-selected, and role="tabpanel". Only the selected tab is in the Tab order (tabindex="0", others -1), and arrow keys move between tabs, as in the toolbar example in Keyboard Interaction. The Tabs project puts it together.

A site navigation menu is a list of links (<nav> with <ul>) plus a toggle button using aria-expanded. It is not an application menu with role="menu", which promises arrow-key behaviour that most site menus do not implement.

Carousels and auto-moving content

If something moves automatically for more than five seconds, give a pause button, and do not start moving at all when the user prefers reduced motion.

Test, test, test

  • Keyboard only.
  • A screen reader for 5 minutes.
  • Zoomed to 200%.
  • With a reduced motion setting.

The W3C ARIA Authoring Practices Guide shows tested patterns for most widgets.

info
Accessibility is part of "done"
A widget that works only with a mouse is not finished. Building it right the first time costs less than fixing it later.