Up front: what
ariaNotify()produces is sound, not anything you can see. I tested the API's behavior in Chromium, but how (and whether) a screen reader speaks depends on the browser and screen reader combination, so the most reliable check is turning on a screen reader and listening yourself. If I've gotten something wrong, corrections welcome.
Intro
This post started with my own site.
My products page can be filtered by role and platform, and sorted. To let screen reader users know "the content changed," I had wrapped the whole list in aria-live="polite". It seemed reasonable, but it's a common anti-pattern: change a filter and the whole list changes, so a screen reader may read every card from top to bottom. What people actually need to hear is a single sentence: "Showing 8 items."
ariaNotify(), from my CSS & HTML cheat sheet, solves exactly this, so I wrote this up while fixing my own site.
First, here's what your browser supports:
Your browser's support
ariaNotify()Detecting (requires JavaScript)
1. The old way: aria-live and its traps
To have a screen reader announce status messages like "Saved" or "8 results found," this used to be the only option:
<div aria-live="polite" class="visually-hidden" id="status"></div>
status.textContent = "Added to cart";
Everyone uses it, and it's famously unreliable:
| Trap | What happens |
|---|---|
| The region must already exist | Content put into a newly inserted live region often isn't spoken the first time |
| It can't really be hidden | Hiding it with display: none or hidden breaks it; only visually-hidden works |
| Identical text isn't repeated | Press "Add to cart" twice; the text doesn't change, so the second one may be skipped |
| Modals block it | With a modal open, the background is inert, so a live region there goes silent |
| Too large a region is noisy | My site's case: wrap the whole list, and every update reads the lot |
2. How to use ariaNotify()
ariaNotify() asks the screen reader to speak a message directly, with no element on the page:
document.ariaNotify("Changes saved");
You can also send it from an element:
saveButton.ariaNotify("Changes saved");
There's a priority option too:
form.ariaNotify("Payment failed, please check your card number", { priority: "high" });
priority | Effect | Roughly like |
|---|---|---|
normal (default) | Waits for whatever is being read to finish | aria-live="polite" |
high | Tries to jump the queue | aria-live="assertive" |
In my Chromium tests, priority accepts only these two values; anything else throws an error rather than failing silently.
3. Try it
The demo has four scenarios: adding to cart repeatedly, a high-priority error, an English announcement sent from inside a lang="en" block, and an announcement from inside a modal. You can also check "Force the aria-live fallback" to compare the two approaches:
Live demo: turn on a screen reader (VoiceOver on Mac is โ+F5), then press the buttons. The log shows what was sent
Cart: 0 item(s) (press it a few times; each press should be announced)
Announcement log (newest first)
- No events yet. Try the buttons above.
This is an interactive demo. Try it on the web page.
If you can see the screen, the log below lists what was sent each time and whether it went through ariaNotify or a live region. The real effect still needs a screen reader; on a Mac, โ+F5 turns on VoiceOver.
4. Element vs. document: it's about language
This is the point I think is easiest to miss, and it matters a lot on bilingual sites:
document.ariaNotify(): spoken in the page's language.element.ariaNotify(): spoken in the language of the element's nearest ancestor withlang.
My site is bilingual. If an English announcement comes from an English block on a Chinese page, the element version lets the screen reader switch to English pronunciation. In the demo, the "Add to wishlist" button sits inside lang="en".
So my habit is to prefer the element version and send the announcement from the button or block that triggered it, so the language follows the content.
5. Without support: a fallback
ariaNotify() is supported from Chrome 141 and Firefox 150; the sources I found disagree about Safari, so test it yourself. Browsers without it still need a live region fallback. This is the code my site and the demo actually use:
const REGION_ATTR = "data-announcer";
function getRegion(host, priority) {
// With a modal open the background is inert, so the region must go inside the dialog
const container = host.closest("dialog[open]") ?? document.body;
let region = container.querySelector(`:scope > [${REGION_ATTR}="${priority}"]`);
if (!region) {
region = document.createElement("div");
region.setAttribute(REGION_ATTR, priority);
region.setAttribute("aria-live", priority === "high" ? "assertive" : "polite");
region.className = "visually-hidden";
container.append(region);
}
return region;
}
function announce(host, message, priority = "normal") {
if (typeof host.ariaNotify === "function") {
host.ariaNotify(message, { priority });
return;
}
const region = getRegion(host, priority);
// Clear first, so identical messages are spoken again
region.textContent = "";
// Content put into a brand-new live region right away is often skipped, so wait a moment
setTimeout(() => {
region.textContent = message;
}, 100);
}
This fallback handles each trap from section 1:
- The region must already exist: besides the delay, I also call
getRegion()once when the page loads, so the live region is in place early. - Identical text isn't repeated: clear, then fill.
- Modals block it: when a
<dialog>is open, the region goes inside it. - Visually hidden: use visually-hidden rather than
hidden, so screen readers can read it.
6. What I changed on my site
Back to the products page. Before, the whole list was a live region:
<div class="group-list" aria-live="polite">
<!-- every product card -->
</div>
After, the list no longer has aria-live. Instead, when a filter changes, the filter controls send one announcement:
watch([sortorder, role, platform], async () => {
await nextTick();
announce(filtersEl, t("data.resultCount", count));
});
The message depends on the count: "No matching items" when nothing matches, and "Updated, showing 8 items" otherwise. Screen reader users who change a filter now hear one sentence instead of a whole row of cards.
The "Edit note" demo in my dialog post uses the same announce(): once the dialog closes, nothing on screen says "saved," so it announces "Note saved."
7. Things to watch out for
- Don't flood people:
ariaNotify()can speak without any user interaction, which makes it easy to abuse. Use it only when people genuinely need to know and nothing else on screen tells them. highdoesn't always jump the queue: an NVDA issue reports that in Chrome 149 and Firefox 155,highdidn't move ahead, and the spec doesn't mandate the order. Don't rely onhighto convey sequence.- It doesn't replace focus management: when focus should move, move it, for example when a dialog opens.
ariaNotify()informs; it doesn't guide. - It can be blocked inside iframes:
Permissions-Policycan disallowaria-notify, and blocked calls fail silently. - Browser support doesn't guarantee screen reader support: always test with VoiceOver or NVDA.
Conclusion
In a few lines:
- Use
ariaNotify()for status messages (saved, added, N results) instead of hiding a live region. - Prefer the element version; the language follows
lang, which matters especially on bilingual sites. - Save
priority: "high"for genuinely urgent messages, but don't rely on it jumping the queue. - Fall back to a live region in browsers without support: clear then fill, fill after a delay, and put it inside an open dialog.
- An oversized
aria-liveregion is noisy. A whole list wrapped in one, like on my site, can become a one-sentence announcement.
After all these years in accessibility, aria-live has always felt like the most unpredictable tool to me: getting it right doesn't guarantee it's spoken, and getting it wrong often goes unnoticed. ariaNotify() finally makes this direct, and precisely because it's so direct, it deserves to be used with restraint.
Where does your site use aria-live? I'd love to hear about it.
Related reading
- How to Write a Dialog Today: From showModal() to command and closedby
- Why Can't Ctrl+F Find Collapsed Content? A Look at hidden="until-found"
- Popover auto, manual and hint: What's Actually the Difference? I Built Demos to Find Out