Lesson 17 · Part 2 · Interactive HTML

Accessible JavaScript

HTML gives you keyboard support, roles and focus for free. JavaScript can quietly take all of that away. This lesson shows the six places scripts most often break a page, and the small fix for each.

In one minute

In plain words

HTML is a house with doors, labels and light switches already fitted. JavaScript is the renovation crew. A careless crew paints over the switches and moves the doors. Your job is to renovate without locking anyone out.

1. The problem: a click handler on a div

A food-delivery app has an "Add to basket" box made from a <div> with a click handler. A mouse user taps it and gets food. A keyboard user presses Tab and never lands on it. A screen reader user hears plain text, not a button. They go hungry.

A click handler is a function that runs when something is clicked. On a real <button>, the browser also runs it for Enter and Space. On a <div>, it runs only for the mouse.

Step 1. Choose the markup. Step 2. Press Tab to reach the control, then press Enter and Space. Watch the log.


          

Masala dosa · ₹120

What happened

    The middle option is a trap. tabindex="0" makes the div focusable, but Enter still does nothing. You would have to add a role, two key handlers and a focus style. The button already has all of it.

    Check
    1. In the lab, which option lets you add the dosa with Space? Only one does.

    2. Keyboard events: handle the right keys, and only those

    A keyboard event (keydown) tells your script which key was pressed. Use it to add keys people expect. Do not use it to steal keys that the browser or a screen reader already uses.

    Which keys a script should handle
    KeyWhereWhat it should do
    Enter, SpaceButtonsActivate. A real <button> does this for you.
    EnterLinksFollow the link. Space scrolls the page instead.
    EscapeMenus, pop-ups, dialogsClose it and return focus to the control that opened it.
    Arrow keysComposite widgets only: tabs, menus, listboxes, gridsMove between the parts. Elsewhere, leave arrows alone. They scroll the page.
    Tab, Shift+TabEverywhereMove to the next or previous control. Do not block it.
    Single lettersShortcutsRisky. Screen readers and voice control use letters too. Let users turn them off or change them (WCAG 2.1.4).

    A composite widget is one control made of several parts, like a set of tabs. Widget roles shows the full key patterns. In browse mode, most screen readers catch arrows and letters before your page even sees them. That is normal.

    // Close a pop-up with Escape and give focus back
    popup.addEventListener("keydown", function (event) {
      if (event.key === "Escape") {
        popup.hidden = true;
        openButton.setAttribute("aria-expanded", "false");
        openButton.focus();
      }
    });
    Try it yourself
    1. Open any menu on a website you use. Press Escape. Does it close? Where is focus afterwards?

    3. Managing focus after the page changes

    Focus is the spot where the keyboard is "pointing". If script deletes the focused element, focus has nowhere to stand. The browser usually drops it onto <body>, the whole page. A screen reader may go silent or jump to the top.

    The fix is one line: after removing an item, call .focus() on something sensible. Usually that is the next item, or the previous one if it was last.

    Step 1. Choose a behaviour. Step 2. Press Tab to a "Remove" button and press Enter. Read the focus log.

    Your basket

      Focus log

        function removeItem(li) {
          var next = li.nextElementSibling || li.previousElementSibling;
          li.remove();
          if (next) next.querySelector("button").focus();
          else emptyMessage.focus();   // has tabindex="-1"
        }

        The same rule applies when you open something. When a panel or dialog opens, move focus into it. When it closes, send focus back to the button that opened it (see Managing focus).

        Check
        1. Remove all four items in "Remove, then move focus" mode. Where does focus end up, and why is that better than <body>?

        4. Announcing results that arrive later

        Some answers take time: a search, a save, a payment. Sighted users see "Searching…" then "3 results". A screen reader user hears nothing, unless you use a live region.

        A live region is an element the screen reader watches. When its text changes, the new text is read out. role="status" is a polite live region: it waits until the user stops typing or reading.

        The region must exist first

        Put the empty live region in the HTML when the page loads. Then change its text. If you add the region and its text at the same moment, many screen readers miss it.

        Step 1. Choose a version. Step 2. Type "ma" in the search box. Try "xyz" too, and tick "Pretend the network fails".

        
                  

          For an error that needs action now, role="alert" is stronger: it interrupts. Use it rarely. For most updates, role="status" is right (see Live regions).

          Check
          1. In "Region on the page first" mode, what does the simulated screen reader say for "xyz"? Is that more useful than silence?

          5. Keep state attributes in sync

          A disclosure is a button that shows or hides a panel. Its aria-expanded attribute tells screen readers if the panel is open. The browser does not update it for you. If your script shows the panel but forgets the attribute, the screen reader says "collapsed" while it is open.

          Step 1. Choose a version. Step 2. Press the "Delivery details" button a few times. Compare what you see with what the screen reader says.

          
                    

          The same goes for aria-pressed on toggle buttons, aria-selected on tabs, and aria-current on the current page link (see States and widget properties). A good habit: write one function that sets the look and the attribute together.

          Check
          1. In "Out of sync" mode, open the panel. What does a screen reader user think will happen if they press the button now?

          6. Respect reduced motion in script too

          Some people get dizzy or sick from movement on screen. They can switch on "reduce motion" in their device settings. CSS can read this with @media (prefers-reduced-motion: reduce). JavaScript animations need to check it too.

          var calm = window.matchMedia("(prefers-reduced-motion: reduce)");
          function addToBasket() {
            if (calm.matches) { showBadgeInstantly(); }
            else { flyBadgeToBasket(); }
          }
          // The setting can change while the page is open:
          calm.addEventListener("change", updateAnimations);

          Step 1. Press "Add to basket". The badge only moves if your device allows motion.

          Basket: 0

          Try it yourself
          1. Turn on "reduce motion" (Windows: Settings › Accessibility › Visual effects › Animation effects off. Mac: System Settings › Accessibility › Display › Reduce motion). Reload and press the button again.

          7. Do not trap focus, and work before script loads

          A keyboard trap is a spot that Tab can enter but not leave. WCAG 2.1.2 forbids it. Scripts cause traps by accident: a handler that cancels Tab, or one that pulls focus back on blur.

          Accidental trap

          Bad
          input.addEventListener("keydown", function (e) {
            if (e.key === "Tab" && !isValid()) {
              e.preventDefault(); // user is stuck
            }
          });

          Let them leave, show the error

          Fixed
          input.addEventListener("blur", function () {
            if (!isValid()) showErrorText();
            // focus moves on as normal
          });

          A modal dialog may keep focus inside on purpose, but only with a way out, like Escape. <dialog> opened with showModal() does this for you.

          Progressive enhancement means building the plain HTML version first, then adding script on top. If the script fails or loads slowly, the page still works.

          Needs script to work

          Bad
          <span onclick="openMenu()">Menu</span>
          <div onclick="search()">Search</div>

          Works first, better with script

          Fixed
          <a href="/menu">Menu</a>
          <form action="/search" method="get">
            <label for="q">Search</label>
            <input id="q" name="q" type="search">
            <button>Search</button>
          </form>
          Try it yourself
          1. Press Tab from the top of this page to the bottom. Did you ever get stuck? (You should not.)

          Common mistakes

          You seeWhy it is a problemFix
          div.onclick for an actionNo keyboard, no role.Use <button>.
          li.remove() on the focused itemFocus falls to the page; user is lost.Focus the next item or a heading.
          "3 results" painted into a plain divScreen reader users never hear it.role="status" present from page load.
          Panel shown, aria-expanded unchangedScreen reader says the opposite of the screen.Update look and attribute together.
          Arrow keys or letters hijacked on the whole pageBreaks scrolling and assistive tech shortcuts.Only inside composite widgets, or make shortcuts optional.
          JS animation ignores reduce motionCan cause dizziness or nausea.Check matchMedia first.

          Remember