Made by Dhawal Made by Dhawal

Screen reader not announcing your toast? Fix it with aria-live (The Silent Toast)

Your toast says Saved, but screen reader users hear nothing. Sanjaya, Domlok's Ether guide for accessibility, breaks the Silent Suzu's spell: keep an empty role="status" live region on the page from load, then change its text so it speaks.

Told by Sanjaya, with Kai asking the questions

The Silent Suzu, a bronze bell yokai with its mouth stitched shut, beside the title "Screen reader not announcing your toast?"

Sanjaya: Your eyes saw the message, Kai. Your ears heard nothing. For a screen reader user, that toast never happened.

But it says Saved right there on the screen!

If your screen reader is not announcing your toast, the usual reason is that the message is not inside a live region that was already on the page. Put an empty role="status" element in your HTML when the page loads, then change its text when you want it to speak.

In the world of Domlok this is the work of the Silent Suzu, a temple-bell yokai with its mouth stitched shut. It lets every message shine on the screen and swallows the sound. Nothing breaks, nothing logs an error. Half your users just never find out the thing they did worked.

Your guide for this episode is Sanjaya, the Ether guide of Domlok and its narrator. He tells every story aloud, so a message that is seen but never spoken is exactly his kind of problem: accessibility is his domain. Kai, as usual, is the one who shipped the silent toast.

The toast that nobody heard

Kai’s settings page shows a little “Saved” toast after the Save button is pressed. He builds it on the fly:

toast.js
function showToast(msg) {
   const t = document.createElement('div');
   t.className = 'toast';
   t.textContent = msg;
   document.body.append(t);
}

KaiIt slides in, it fades out, it looks great. What’s the problem?

A screen reader reads where the user is. After pressing Save, that is still the Save button. A new div appearing at the end of the body is just more content on the page. Nothing tells the screen reader to stop and read it, so the user hears “Save, button” and then silence.

Live regions: aria-live, role=”status” and role=”alert”

A live region is an element the screen reader keeps watching. When its content changes, the change is announced, wherever focus happens to be.

Markup What it means
aria-live="polite" Announce changes when the screen reader has finished what it is saying.
aria-live="assertive" Interrupt and announce straight away.
role="status" A ready-made polite live region. Implicitly aria-live="polite" and aria-atomic="true", so the whole message is read, not only the part that changed.
role="alert" A ready-made assertive live region, implicitly aria-live="assertive" and aria-atomic="true". For urgent messages only.

KaiEasy. I add role=”status” to my new div and I’m done.

toast.js
function showToast(msg) {
   const t = document.createElement('div');
   t.setAttribute('role', 'status');
   t.textContent = msg; // full on arrival
   document.body.append(t);
}

SanjayaTry it with a screen reader on. Often you still hear nothing.

Why a new live region stays silent

Screen readers announce changes inside a live region. A region that is inserted with its message already inside never changed while anyone was watching it, so many browser and screen reader combinations say nothing. MDN’s advice is to establish the live region first, ideally in the initial markup, and update its content afterwards.

The fix: be on the page before you speak

Put one empty live region in your HTML. It is there from page load, so the screen reader is already watching it. To show a toast, change its text.

index.html
<div id="toast" class="toast" role="status"></div>
toast.js
const toast = document.getElementById("toast");
function showToast(msg) {
   toast.textContent = "";
   setTimeout(() => {
   toast.textContent = msg;
}, 100);
} 

Clearing the text first and setting the message a moment later matters when the same message repeats. Setting “Saved” over “Saved” is not much of a change, and some screen readers skip it. Clearing, then setting it in a later task, gives them a real change to announce.

MDN also suggests adding a redundant aria-live="polite" to role="status" for the widest support. That is fine for status. Do not do the same with role="alert" (see below).

KaiBut an empty toast still sits on the page. Can I hide it with display: none until it has text?

SanjayaNo. Content hidden with display: none, visibility: hidden or the hidden attribute is not read, and a region that only appears when its text arrives is a new region again. Hide the empty toast with opacity.

toast.css
.toast { transition: opacity .2s; } .toast:empty { opacity: 0; } /* still in the accessibility tree */

Status or alert?

  • role="status" for news that can wait: Saved, Copied to clipboard, 3 results found. It waits for a pause.
  • role="alert" only when the user must know right now: a payment failed, the connection dropped. It interrupts, so MDN asks you to use it sparingly.
  • Don’t stack aria-live on role="alert". The role is already assertive, and adding both can make VoiceOver on iOS read the message twice.
  • Keep buttons out of toasts. Live regions are for text. An Undo button that vanishes after three seconds is hard to reach with a keyboard or a screen reader. Put actions somewhere that stays.
  • Don’t move focus to the toast. A status message should inform without pulling the user away from what they were doing.

Listen for yourself

1. Check the region in DevTools. In Chrome, select the toast element in the Elements panel and open the Accessibility pane. Before anything is shown, it should already list it as a live region (polite).

2. List every live region on the page. Run this in the Console before you press Save. If your toast is not in the list yet, it will not be announced.

DevTools console
document.querySelectorAll("[role=status], [role=alert], [aria-live]"); // NodeList [div#toast]

3. Use a real screen reader. On a Mac, turn on VoiceOver with Cmd + F5. On Windows, NVDA is free. Press Save, keep your eyes off the screen, and listen. Try it twice in a row too.

KaiSaved! I heard it, both times!

Before you ship a toast

  • Is the live region in the HTML from page load, not created with its message inside?
  • Is it role="status" for ordinary news, and role="alert" only for urgent problems?
  • Is the empty region hidden with opacity, never display: none?
  • Does the same message, shown twice, get announced twice?
  • Have you heard it in VoiceOver or NVDA?
Acharya's Rule #24

Put the live region on the page first. Then change its text, and it will speak.

Next episode: The Shrinking Scroll. Why a flex item with a long word or a code block refuses to shrink and overflows its card, and what min-width: 0 has to do with it.

Think you can make the Suzu ring? Play Sanjaya’s Bell on domlok.in and earn seal 24.

Watch the full episode and the Short on YouTube at @dom-lok and save the carousel on Instagram at @mbd.domlok.

Further reading

Frequently asked questions

Why is my screen reader not announcing my toast message?+
A screen reader reads where the user is, so a message that appears elsewhere on the page is not spoken unless it is inside a live region. The live region also has to be on the page before its text changes; one that is inserted with the message already inside is often not announced.
What is the difference between role="status" and role="alert"?+
role=”status” is a polite live region: it waits until the screen reader finishes speaking, which suits messages like Saved or Copied. role=”alert” is assertive and interrupts immediately, so use it only for urgent problems such as a failed payment.
Do I need aria-live if I use role="status"?+
No, role=”status” already behaves like aria-live=”polite” with aria-atomic=”true”. MDN suggests adding a redundant aria-live=”polite” for the widest support, but do not add aria-live to role=”alert”, because VoiceOver on iOS can then read it twice.
Why is the same live region message not announced twice?+
Setting the same text again may not count as a change, so some screen readers skip it. Clear the region’s text first, then set the message again in a later task, for example with setTimeout.
How do I test aria-live announcements?+
In Chrome DevTools, select the element and check the Accessibility pane lists it as a live region before anything is shown. Then turn on a real screen reader, such as VoiceOver on a Mac (Cmd + F5) or the free NVDA on Windows, trigger the message and listen.