If ever there were a statement that is going to be tough to live up to, that heading right there is it. I have really made a rod for my back, as almost nobody under the age of 50 would say. For those under 50 what I mean is this: I’ve set myself a really hard challenge and should you choose to read on you’ll find out whether I’ve screwed it up or not (or perhaps somewhere in between).
Why Oh Why Am I Doing This?
A while back, the thorny issue of how some ‘toast’ messages were being used in a client’s site came up, as they often do. The advice I gave was … not totally helpful. Why so? Because there was not really a solution that I could offer that solved the problem they had without then causing a different kind of problem. Still, I had to raise the issues as they existed and promised that I would do some more research into a solution. Much, much later, I’ve finally put down some thoughts and tried out some ideas (hint: this blog post here, this is it). So this article is primarily aimed at developers who have been tasked with adding this type of notification in a way that won’t upset some users, but also as a reference to accessibility auditors looking to explain to clients what issues these typically present.
I’ll summarise some of the problems with the toast messages as I found them on the site momentarily, but in case you want a total, thorough run-down of everything wrong with toast messages to ruin your day, please read Adrian Roselli’s article Defining ‘Toast’ Messages.
If you have read that then came back to here, you’ll realise why I might have been foolish to try taking it on. I feel like I have to try, though. Otherwise I’ll have to keep on raising issues on clients’ sites that say “Hey, this is a problem … but good luck fixing it, LOL”.
What’s Wrong With Toasts?
Before I summarise the problems, let’s just state what a ‘toast’ is.
Ok, Fine. What’s a Toast?
A toast is a building designed for kilning (drying) hops as part of the brewing process. They can be found in most hop-growing (and former hop-growing) areas and … wait. That’s wrong. That’s actually an oast, and I just threw that in there because 1, I think it’s a funny little name for a building and 2, I can then legitimately say that someone will have learned something in this article.
Please, Just Tell Me What a Toast Is Already
It’s a message that appears on screen, often with some kind of animation (for example ‘pops up’ from the bottom right corner of the screen, like … like … oh yeah, let’s call it toast, shall we?). Truth is, people call a lot of these interactions ‘toasts’ when often they are not, really. We’ll get on to that more later. But in a nutshell, it’s a simple message that appears somewhere on screen, usually to provide confirmation of some action that the user initiated:
- “Added to basket”.
- “Timer extended by 5 minutes”.
- “Skeletons in closet successfully deleted”.
The image above shows the kind of thing. It’s just a simple text message, although some of you may have scratched your chin momentarily and thought to yourself “Wait a minute, there is a heading and a paragraph here … so it’s not just plain text”. All true. But if the small amount of content here were inside a live region and announced by a screen reader, the structure is not massively important; the message is.
But what if …
OK, there’s another thing that snuck in there. That ‘Undo’ button? That just de-toastified everything. This is no longer a toast, no matter how or where you’re displaying it. Let’s find out why.
Toast Roll
Before we dig deep into this, first I want to cover the types of messages that I come across that are often called toasts, not always correctly. Here’s a short list with some names I just made up on a whim because it amuses me:
- The One True Toast: A simple message that appears briefly, then automatically disappears.
- The Lurker Toast: A simple message that appears but outstays its welcome, and therefore needs to be dismissed (so it has a ‘close’ button).
- The ‘It’s Complicated’ Toast: A toast that not only has a ‘close’ button, but has other interactive controls inside.
To revisit an old Sesame Street classic for a moment, one of these things is not like the other, and it’s the first one. Because, well … it’s the only one that’s actually a toast.
The One True Toast
The user does something on the page (presses a button) and a message appears briefly to confirm that the thing they wanted to happen did happen:
“Great success!”
The message pops off again very quickly and nobody was harmed. Or were they?
“I couldn’t read that message, it flashed up too quickly. What did it say? WHAT DID IT SAY? DID I MESS THINGS UP?!”
So much for providing a reassuring message. Seems to have had the exact opposite effect. Well done, Captain Anxiety, now look what you did.
Or perhaps the user has low vision and because of this only a portion of the screen is visible to them. And the message that was shown in the toast? Yup, that was in the out-of-view part of the screen:
A few seconds later, without their knowledge, the buyer sealed those puppies’ fate.
Before I continue, though, I reckon there are a few of you who might have been reading the last few lines, mulling things over but in your head a phrase is repeating itself: “The example you gave is not a simple text message! The example you gave is not a simple text message! The example you gave is not a simple text message!”
Agreed. That’s definitely an image of Borat giving a double thumbs up. But if it were set as a decorative image with alt=""
, as per the markup below, if injected into a live region it would still be announced perfectly well in a screen reader with just the text elements:
<div role="status"">
<!-- ^ Live region-->
<div class="toast" id="toast1">
<h2>Great Success!</h2>
<img src="great-success.png" alt="">
<p>The thing that you wanted to happen? It worked</p>
</div>
<!-- ^ Injected content-->
</div>
“Great Success! The thing that you wanted to happen? It worked”
I put this in there just to make it clear that a simple toast message does not have to be devoid of any styling or character.
If Only There Were More Time …
Back to the crux of the issue here, then: time. The messages that are disappearing too quickly are a problem for people who can’t read the contents quickly enough (spoiler alert: that could be almost anyone), or people who can’t see it at all because they are viewing a different part of the page and will never know it appeared. This is a failure of SC 2.2.1 Timing Adjustable (Level A). If you use this type of fleeting message but also provide a mechanism in the app or site’s preferences such that the user can have these messages stay on screen longer, you won’t fail 2.2.1. Also, if you do make it possible for the message to stay forever, it will need a close button, which means it is no longer a toast (more on that shortly).
What if I Can’t See the Page at All?
It matters not if the message is shown for 5 seconds, 20 seconds or a minute if you can’t see the screen, though. So for a brief, fleeting message like this, it needs to be implemented as a live region or a status update. This ensures that the message shown on screen is also made available to assistive technology users and will be announced by a screen reader when it is visually presented on screen. Done right, this will avoid getting a slapped wrist for failing SC 4.1.3 Status Messages (Level AA).
The Lurker Toast
This is a type of message that pops up wherever it is on screen, and stays there. It won’t budge until the user explicitly dismisses it by activating a ‘close’ button or pressing ESC key (assuming that support has been built in for that).
Typically, this type of message is placed in a fixed position on the screen. Scrolling up or down makes no difference, it will stay anchored to the spot. Ironically, although this is more accurately mimicking the behaviour of a real piece of toast (pops up and stays there – after all, who owns a toaster where the toast pops back in after a few seconds? Honestly … who names these things?), this is no longer a toast message. The reason for that? The presence of an interactive element inside the container.
By adding this ‘close’ control, and keeping the message permanently visible until that control is activated, it is now presenting a new set of issues for anyone who is not a clear-sighted mouse user.
Also, because of the way that toast messages are implemented using live regions, where the structure and semantics of any content inside is essentially ignored by assistive technology, the ‘close’ control would be announced as ‘close’, but without any indication that it is a button. A user might infer from the wording that this is an actionable thing, but they can’t do anything with it, at least not without potentially a lot of key presses.
TAB, TAB, TAB, Is This Thing Wrong?
If you’re using a keyboard to navigate, and that would include screen reader users, you will have to TAB to that ‘close’ control to remove the message. Because of the way that these type of messages are rendered on the page, typically this means that they are injected into the document right at the very end, just before the closing tag. This is to ensure that there are no z-index
problems, so that the message will always appear over the top of all other content. Fine. So what’s the problem? The issue here is that there may be a whole heap of keyboard-focusable elements between the point where the user did something to initiate that message, and the message itself. In short, the user might find themselves having to TAB numerous times to dismiss the message.
Providing the capability to dismiss it with the ESC key is good, but not everyone will know or assume that that’s an option. Furthermore, as these messages stay on screen until dismissed, there is a danger that another interaction on the page that responds to ESC could be triggered by accident.
Shall We Have a Little Dialog About This?
Perhaps, instead of forcing the user to TAB all the way through to this message to dismiss it, you could move the focus directly to it instead, placing the focus directly on the ‘close’ control. That’s great, sounds perfect. Well done, what you’ve actually done here is reinvent the dialog. Just to be clear, using a dialog for this is perfectly acceptable, but you should know the following:
- If you are moving focus to this new content, it must not be set up any more as a live region. If it is a live region and also implemented as a dialog, you run the risk of having the message announced twice by screen readers. So it must be one or the other: live region/status or dialog.
- If you are going to handle this as a dialog, take extra care with focus management. Often these notification messages occur as a result of a destructive process. For example, deleting an entry also removes the button that was clicked to initiate that deletion. If you shift focus to the confirmation dialog, when that dialog is dismissed you need to move the focus back to a logical place on the page. Once again, uncle Adrian has a very thorough post that goes through your various options in Where to Put Focus When Deleting a Thing.
The ‘It’s Complicated’ Toast
Adding a ‘close’ button might seem harmless enough, but as explained above, by doing that you cannot claim to call that thing a toast anymore. Before you know it, you’re throwing all manner of other features into the mix.
If there were any doubt before, there is no doubt about it in my mind at this point: this is a dialog. Stop calling it a toast. Stop trying to make toast happen.
And if you think that’s getting too complicated for a humble toast message, imagine the fun when there are multiple instances that start stacking up:
This is the Lurker Toast with bells on. And whistles. And that’s just annoying. All of the problems of the previous example (with just a ‘close’ button), but multiplied.
So it’s a dialog. Uh-oh. That seems all well and good if you have one of them, but if you have multiple updates, then this might get a little messy. Or as a wise meme once said …
So, What the Heck Do We Do Here?
I’ve spent an awful lot of time saying what could be boiled down to:
- This is a toast.
- These are not toasts.
But I wanted to cover the features that these interface elements have, which I see regularly when auditing sites, and to acknowledge and understand why they have come about. That gives me the guidance on how we might develop something that can work from the simplest text message to the more complicated offerings while also satisfying the various WCAG SCs that they typically fail. A bit of status update, a bit of toast and a bit of dialog. I think I’ll call it a StaToastAlog.
Let’s Define Some Rules
Based on the findings and observations above, this is what I think:
- Simple text update:
- It’s a toast.
- With a live region.
- But it MUST have user-adjustable timer settings (or it fails 2.2.1 Timing Adjustable).
.
- Text update with a close button:
- It’s a dialog.
- Focus moves to the item, no need for live region.
- Care needed to move focus somewhere useful after dismissing.
.
- Text update with some other interactive elements inside:
- It’s a dialog.
- Focus moves to the item, no need for live region.
- As above, care needed to move focus somewhere useful after dismissing.
- In other words … it’s the same as the previous example, it’s just got more interactive elements in it.
.
Let’s Build This Thing!
So, my mission here was to create something that basically looks the same regardless of which of the three scenarios it falls into. Most people will have a difficult time identifying when something is a toast and when it’s a dialog. Most people won’t even really care, but as long as there is a consistent behaviour with these things (up to a point), how it’s implemented will not be that important for a large number of people.
That said, for some people it is incredibly important that certain behaviours are built in which are very much tied to how they are implemented. The key things here are regarding focus management. I’ll make the whole script available at the end (which you have my permission to laugh at given that my JavaScript style is an embarassing mix of ‘I started doing this in the 90s’ and ‘I copied stuff I don’t fully understand a few years back but it seems to work’).
A Little Bit of Toast
Let’s start with an actual, real toast.
If there is nothing interactive here (this is the case) and the message is just text—ignoring the fact that there is a heading and a paragraph—then the text displayed can, and should, be implemented as a live region.
Note that there is no ‘Close’ button here. This message will auto-disappear. As mentioned earlier, though, this will give you a failure of SC 2.2.1 Timing Adjustable. Unless, of course, you provide some kind of mechanism to adjust that timing. For the purposes of the proof of concept, I have set this as a measly 5 seconds.
Something worth noting – live regions need to be on the page before content is injected into them, otherwise screen reader users will not get the notification that they need. There are a couple of options to address this:
- Have a general status container that is present on all pages by default, always ready and available to accept updates that happen to it OR.
- Dynamically create the live region as needed, but add a slight delay before populating it.
This script opts for the second choice:
//Dynamically create a live region (which will not be visible, not receive focus)
const notificationsLiveRegion = document.createElement('div');
notificationsLiveRegion.setAttribute('role', 'status');
notificationsLiveRegion.setAttribute('id', 'toasts-status');
notificationsLiveRegion.setAttribute('class', 'visually-hidden');
document.body.appendChild(notificationsLiveRegion);
...
//Add the toast content after a short delay to be SURE it will be announced
setTimeout(function(){
toastHeading.textContent='Update successful ' + toastCount;
toastPara.textContent='You added the thing';
//... and show the toast (was hidden until message populated)
toast.classList.remove('hidden');
},250);
A slight delay—250 milliseconds—is fast enough for things to feel snappy, but also slow enough for assistive technology to get its head around the changes in the page and to provide this as a status update to the user.
If I Could Turn Up Time…
Let’s say that we’ve taken care and attention and allowed the user to adjust the timing of these things. Let’s say that it’s possible to get these messages to hang around for 20 seconds rather than 5 or, even better, allow the user to enter a value completely of their choosing. Now let’s imagine that it’s a site where, in some cases, you might have a series of these in close succession. Before you know it, the whole page could be plastered with these things. And if they’re going to hang around for 20 seconds or more, this could be an issue.
Don’t Leave Me This Way
These are just toasts, no interactive content inside, so we made the ‘clever’ decision to not tease someone with a ‘Close’ button that would be about as much use as a chocolate teapot for most people. If it’s set to auto-dismiss after 5 seconds, keyboard users will never navigate to it quickly enough to use and, to be fair, most mouse users will struggle to find the target and click it in that time. But now you’ve got a page full of them and they’re hanging around like a bad smell. What to do? Reload the page? Just wait it out?
In my opinion, if you have chosen to set such messages to stick around for longer, you probably need to provide a mechanism to manually dismiss them too. Like this:
Earlier I stated that if there is a close button, then it’s a dialog. However, I think there’s a middle ground here. This is where I blur the lines somewhat.
If there is a mechanism to change the notification timing, for example setting it to 20 seconds instead of a measly 5 seconds, then I would suggest that this doesn’t have to be a dialog, that it doesn’t really benefit from shifting focus to it. The close button is still useful for mouse users (who have already made a conscious decision to change the timing from the default 5 seconds, and so will know this is the case). But you may be thinking “If there’s a close button there, that will be announced in the status update, won’t it?”. It would if the visible content is the status container. However, in my proof of concept (link at the end), I’ve implemented it like this:
- There is a wrapper element that contains all the visible notifications, but it does not have
role="status"
. - There is a separate node added which is used for status updates (which is visually hidden).
- When the notification is shown, depending on the content inside it it does one of two things:
- Simple text notifications are shown visually (no focus management, not a dialog) and the same text is injected into the visually-hidden status node.
- Notifications that include interactive elements are updated visually, treated like dialogs and with focus management, and the visually-hidden status node does not get updated. This is also true if the notification is a simple text notification (no interactive elements) but where the adjustable timing has been set to ‘never’ auto-dismiss. That rule trumps everything.
Doing this means that we don’t have issues with double announcements. It’s either the status that causes the announcement or the dialog receiving focus that does it.
Close, Close, Close, Close etc
In the script, when 1 or more dialogs are added focus moves to the newly added update. If multiple instances are present (which is a possibility), focus is trapped within the collection of updates (not trapped within an individual dialog), so a keyboard user can cycle through all of these and dismiss whichever ones they want to). Also, content underneath is hidden to assistive technology, so these are acting as modal dialogs.
Now, as I noted, on some pages these things can start to take over. While it’s good that we manage focus and allow each one to be dismissed with the keyboard, perhaps we can help just a little more here? For example, if these things are starting to stack up, provide a way to dismiss them in one go. I’ve gone with: if there are three or more, add a ‘close all’ button:
Show video
Note that the video shows me triggering the updates using a mouse, and also activating the ‘Close all’ button with the mouse. So you can’t see the focus management happening here. But trust me, it’s all there. Would I lie to you?
If the ‘Close all’ button is activated, it also ensures that focus is returned to the interactive control that was last used that caused the updates to show.
Push the Button
Here’s where I start to get more opinionated about behaviour (Start? Just started? Ian, have a word with yourself).
Assuming that the user has been provided with a mechanism for extending the timing of these … I won’t say toasts, I won’t say toasts … notifications, if they contain interactive elements (link, button), then that timing for auto-dismissal should just be ignored. If there is something that the user might need to interact with, then the script should force it to behave like a modal dialog.
If it looks like a duck and quacks like a duck, then it’s a dialog, or something like that. You get what I mean.
Jumping to Conclusions
So, how does all that work for you? I tried to create a script/pattern that adapted based on the content that is being displayed and preferences set by the user, with some opinionated decisions thrown in for good measure. The appearance (position/style) of these updates is the same regardless of the content or timings, but how it’s built and exposed changes, as does the focus behaviour.
Try it out for yourself, and feel free add your thoughts in the comments. Bear in mind that this proof of concept is not production-ready, in so far as it have some basic hard-coded text, buttons and links.
Proof of concept (on CodePen), opens in new window.
Some thoughts about how this could be enhanced further:
- If the notification is shown and it can be programmatically determined to be out of view, also add a sonic notification – a gentle ‘ding’.
- Or even have that by default regardless of whether the notification is in the viewport (there are a number of scenarios where it may not be visible such as another floating window is obscuring it, or the browser window position such that part of it is off screen, or screen magnification is in use that a script cannot determine what’s in view).
- For each notification shown, add it to a messages log that is available to review at any time (out of scope for this JavaScript proof of concept – it would need to persist beyond a page reload).
Or perhaps this was all just a frustrating waste of your time and you would rather have been learning about oasts. OK, FINE.
Here’s an oast for you:
Happy now?
Comments