The issue is multiple live regions on the page.
When you add your first snackbar you expose role="alert"
(which is the same as aria-live="assertive"
effectively).
This should work pretty well across all browsers (a single alert).
However, multiple aria-live
regions tend to fail in numerous screen readers, especially when dynamically added and removed.
The best way to deal with this is to move all screen reader alerts into a single aria-live
region and set up a message queue to handle alerts at the application level, this way you only ever have one aria-live
region (2 if you need a polite and an assertive option) and won't run into as many issues.
A quick Example / the concept
HTML
<!-- You can add and remove these at will, this is the dynamically added snackbar -->
<div class="snack"></div>
<!-- This will **always** exist on the page and we will update the contents from our message queue -->
<div id="alertAnnouncer" aria-live="assertive">
</div>
Pseudo code
// Adds the snack bar with the specified text. This has nothing to do with the screen reader message queue, it is just to represent your current function to add a snack bar.
addSnackBar("Alert Text");
// this is how we add messages to the queue.
addScreenReaderAlert("Alert Text");
// we hold our messages here for processing.
var messageQueue = [];
function addScreenReaderAlert(textToAnnounce){
messageQueue.push(textToAnnounce);
//CODE TO ADD: start the interval to process the queue if it is not running, if no update has happened in the last x seconds process the message immediately and set the last time processed or something similar.
}
function processMessageQueue(){
if(messageQueue.length > 0){
//CODE TO ADD: get oldest message (you would use slice etc. and remove it entirely from the queue)
// ultimately we want this
let oldestMessage = messageQueue[0];
// we change the inner HTML of the aria-live region so the oldest message is announced.
return document.querySelector('#alertAnnouncer').innerHTML = oldestMessage;
}else{
//CODE TO ADD: clear the interval
}
//NOTE: to save CPU cycles I would stop the interval once the message queue reaches 0 length and then restart the message queue interval when you fire the addScreenReaderAlert() function.
}
// this is purely to show there is an interval on the queue, you would set and unset this as described above.
Window.setInterval(processMessageQueue, 1000);
// NOTE: The interval delay should be set to a value large enough to allow a screen reader to announce the messages. So for a simple snackbar this can be shorter, but if you want to announce longer messages you need to set this higher. Note that the interval is purely for spreading out multiple messages added in quick succession so you don't overwrite them too quickly.
Explanation of code
That all looks complicated and messy, my apologies!
But essentially we want a way of:
- adding a message to a queue
- processing that queue with a gap between each update to the
aria-live
region.
- Ideally we process items immediately if there hasn't been a message in the last x seconds.
The gap between messages is to ensure you do not overwrite the message currently being spoken.
You can set up aria-live
regions so that you can continue adding messages to them (appending the message rather than replacing), but this is not always stable in all screen readers (older NVDA, ORCA etc. don't always read new items after a certain length). This workaround is pretty robust once you fine tune the message timings etc.
The message queue, although a little bit of work to set up in the first place, is also a super easy way to manage all alerts etc. across your whole application without worrying about multiple alerts overlapping etc.
If you need more explanation (as I am not sure if I "waffled" on in this explanation ) just let me know.