Mayank RSS

Making live regions easier

Live regions are a way to have dynamic content announced by assistive technologies (such as screen-readers). Live regions are essential for building highly interactive web applications, but they are also difficult to use correctly, as Sarah Higley describes in excruciating detail.

Sarah offers some suggestions to make live regions less finicky:

  1. Keep the notification text short and concise. Don’t use special characters or non-text content.
  2. Set a timeout to remove text from the live region, to avoid duplicate or stale announcements.
  3. Insert an empty live region as early as possible, before even sending any notifications.
  4. Don’t send too many notifications at the same time. Prefer static text.

So I’ve decided to build a custom element (if you can call it that) that makes it relatively straightforward to use with these guidelines in mind.

  1. Grab it from npm:

    Code snippet
    npm add @acab/live-announcer
  2. Set it up on page load.

    Code snippet
    import * as announcer from "@acab/live-announcer";
    announcer.setup();
  3. Announce notifications from anywhere on the page, in a rude or polite way.

    Code snippet
    announcer.notify("Something happened!", { priority: "important" });
    Code snippet
    announcer.notify("Something happened, but it can wait.");

Where’s the HTML??

Good question! Short answer: don’t worry about it.

Long version: This whole idea doesn’t make any sense as HTML. We want to announce updates imperatively from other JavaScript code, so this really should have been a built-in JavaScript function in the first place, rather than an HTML element or attribute. In fact, this is how the proposed notification API will work too.

So even though I’m defining a custom element, it will not be used directly in the HTML. Calling setup() will automatically create an instance of <live-announcer> and inject it into the page. Specifically, it will be appended into the <body> element’s shadow tree. In some cases, you might want to inject it somewhere else, for example, into a modal <dialog> or a popout window. For such scenarios, I expose the constructor too:

Code snippet
import { LiveAnnouncer } from "@acab/live-announcer";

const announcer = new LiveAnnouncer();
announcer.setup({ target: document.querySelector("dialog") });

announcer.notify("Something happened inside the dialog!");

I could have gone an alternate route, where I require the <live-announcer> element to already be present in the HTML. However, this would result in a less convenient API, and it might not play nicely with UI frameworks like React.

The LiveAnnouncer custom element

The implementation is actually quite simple.

This custom element is comprised of two visually-hidden live regions, one for assertive updates and one for polite ones.

Code snippet
<div aria-live="assertive"></div>
<div aria-live="polite"></div>

Every time notify() is called, the text will be injected into one of these two live regions. After a short delay, the live region will be cleared so that it’s ready for future updates. Here’s the simplified code:

Code snippet
notify(text, { priority = 'none' }) {
  const region = priority === 'important' ? this.#assertiveRegion : this.#politeRegion;
  region.textContent = text;
  setTimeout(() => region.textContent = "", 3_000);
}

You can find the full code on GitHub.

Demo

Click the buttons to trigger notifications:

Code
Code snippet
assertiveButton.onclick = () => {
  announcer.notify("This is the assertive notification.", { priority: "important" });
};
politeButton.onclick = () => {
  announcer.notify("This is the polite notification.");
};
bothButton.onclick = () => {
  announcer.notify("This is the polite notification.");
  setTimeout(() => {
    announcer.notify("This is the assertive notification.", { priority: "important" });
  }, 500);
};

This has been tested to work in:

I found that some combinations treat “assertive” notifications the same as “polite” (meaning it does not interrupt any ongoing announcements), but it’s not completely broken so I’m ok with that. Certain tweaks to my code might make the “assertive” notifications get announced more reliably, but that’s an excursion for another day.

Also worth noting that I’m carefully using only the most well-supported parts of live regions. For more detailed testing on other aspects of live regions, refer to Scott O’Hara’s article.