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
Sanjaya: Your eyes saw the message, Kai. Your ears heard nothing. For a screen reader user, that toast never happened.
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:
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.
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.
<div id="toast" class="toast" role="status"></div>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 { 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-liveonrole="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.
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, androle="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 #24Put 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.