# Toasts that vanish before you can read or undo them

Source: https://easeweb.dev/learn/toast-timing
Topics: Dialogs and overlays > Toasts are announced and stay long enough (WCAG 4.1.3, 2.2.1)
The fix: Don't put an action people may need behind a timer. Either keep the toast until the person closes it, or put the Undo where the deleted item was, with focus on it.
Test with: screen reader, keyboard, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html https://www.w3.org/WAI/ARIA/apg/patterns/alert/ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

An inbox lists messages, each with a Delete button. Press Delete and the row goes; a dark toast appears in the bottom corner: "Message deleted. Undo". Four seconds later it slides away, and the message is gone for good. There is no Trash. A sighted mouse user who deleted the wrong message sees the toast and clicks Undo in time. Everyone else has to be fast.

The toast is even announced. It is added inside a `role="status"` container that is always on the page, so a screen reader says "Message deleted, Undo". The failure is not that it is silent: it is that it doesn't wait.

## Why it fails

Success Criterion 2.2.1 Timing Adjustable covers any time limit the content sets, and anything that changes or disappears on its own after a set time is one. W3C's Understanding document uses this very case: a mail notification that disappears after five seconds is not a problem when the same information can be found another way, such as by looking in the inbox, but if people have no other way to get it, the timed message must meet the criterion. Here the toast holds the only Undo. To pass, people would need to be able to turn the limit off, or adjust it to at least ten times the default, before meeting it, or be warned and given at least 20 seconds to extend it. None of that exists, so the action has a four-second deadline.

The toast also has to be heard. Criterion 4.1.3 asks that a status message like "Message deleted" reaches assistive technology without moving focus. This toast manages that, but an announcement doesn't help if the thing announced has gone before anyone can act on it.

## Who is affected

How long four seconds is depends on how people use the page. Reading the message, deciding it was a mistake and getting to the button all take time, and the people who need more of it are the ones least likely to see the toast at all. On a phone, the toast often covers the bottom of the list as well, so people may wait for it to leave before carrying on.

## What automation and AI miss

A scan sees a `role="status"` container, a button with a name and good contrast, and passes all of it; timers live in script, and the toast is usually gone before any tool looks. A scan of a page with no toast showing finds nothing at all. AI assistants and component libraries default to toasts that close after a few seconds, often with Undo inside, because that is how the pattern is usually drawn. Some add "pause on hover", which helps only the mouse users who were already fast enough. Only a person deleting something and taking their time, with a keyboard, a screen reader or a magnifier, shows the failure.

## Before: an Undo toast that closes after four seconds

```html
<ul id="inbox">
  <li>Lunch on Friday <button type="button" class="delete">Delete</button></li>
  <!-- … -->
</ul>
<div id="toasts" class="toasts" role="status"></div>
```

```js
function remove(row) {
  const next = row.nextElementSibling ?? row.previousElementSibling;
  row.remove();
  next?.querySelector('button').focus();

  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.append('Message deleted. ', undoButton(row));
  toasts.append(toast);
  setTimeout(() => {
    toast.remove();
    deleteForGood(row);
  }, 4000);
}
```

The only way to get the message back is inside a toast that removes itself after four seconds, and nothing lets people turn that time off, lengthen it or extend it. For anyone who reads, moves or finds the toast more slowly, the Undo has already gone.

## After: a toast that stays until it is closed

```html
<ul id="inbox">
  <li>Lunch on Friday <button type="button" class="delete">Delete</button></li>
  <!-- … -->
</ul>
<!-- Straight after the list, so Tab reaches it soon -->
<div id="toasts" class="toasts" role="status"></div>
```

```js
function remove(row) {
  const next = row.nextElementSibling ?? row.previousElementSibling;
  row.remove();
  next?.querySelector('button').focus();

  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.append('Message deleted. ', undoButton(row), closeButton());
  toasts.replaceChildren(toast);
}
```

The toast keeps its look and its announcement, but no timer removes it: it goes when the person presses Undo or Close, or when the next toast replaces it. People can take as long as they need to read it, Tab to it and decide. Where the toast container sits in the DOM matters: keyboard users reach it by Tab, so put it straight after the content it reports on, not at the end of the page, and keep it from covering the list while it stays.

## After: Undo where the item was

```html
<ul id="inbox">
  <li>Lunch on Friday <button type="button" class="delete">Delete</button></li>
  <!-- … -->
</ul>
```

```js
let pending; // the latest delete, still undoable

function remove(row) {
  pending?.commit();
  const title = row.firstChild.textContent.trim();
  const undo = document.createElement('button');
  undo.type = 'button';
  undo.textContent = 'Undo';
  undo.setAttribute('aria-describedby', `${row.id}-note`);

  const note = document.createElement('span');
  note.id = `${row.id}-note`;
  note.textContent = `Deleted "${title}".`;

  const placeholder = document.createElement('li');
  placeholder.className = 'deleted';
  placeholder.append(note, ' ', undo);
  row.replaceWith(placeholder);
  undo.focus();
  pending = { commit: () => { placeholder.remove(); deleteForGood(row); } };

  undo.addEventListener('click', () => {
    pending = null;
    placeholder.replaceWith(row);
    row.querySelector('button').focus();
  });
}
```

The deleted row is replaced by a line saying what was deleted, with its Undo button. It stays until the person does something else that ends it: deleting another message commits this one, and so does leaving or reloading the page (send the delete then, for example on `pagehide`). Only the latest delete waits, so the list never fills with rows that are gone. The Delete button the person pressed has gone, so focus has to move somewhere; it moves to Undo, and a screen reader says "Undo, button, Deleted "Lunch on Friday"". There is no timer to adjust, no toast to find, and the Undo is where keyboard users, magnifier users and sighted users are already looking. What ends it is the person's next action, not the clock, which is what 2.2.1 asks. This works only where the deleted item has a place to leave the Undo in: one row in a list that stays put. A bulk delete, a list that re-sorts or pages, or a delete from the item's own page has no such place.

## Which option to use

Both pass 2.2.1, and both keep the message announced. Which suits depends on where the delete happens. For one row in a list that stays put, put the Undo where the item was: focus is already on it, nothing floats over the page, and no one has to Tab to find it. Where there is no such place, as with bulk deletes, re-sorting or paged lists, or a delete from the item's own page, use the toast that stays until closed; it is also the quickest change for a site that already shows its messages as toasts. Many sites will use both.

## Implementation decisions

**Pausing on hover or focus is not enough on its own.** It stops the timer once someone reaches the toast, which helps only the people who reached it in time. Do it anyway in a toast that still has a timer: nothing should disappear while a pointer is on it or focus is inside it.

**A Trash can let a toast time out.** If the site already keeps deleted items in a Trash with Restore, a toast that closes by itself passes 2.2.1, because the same action can be done another way, which is the case W3C describes. The toast must say where the item went ("Moved to Trash"), and its timer should stop once a pointer or focus reaches it. People who miss it still take several steps to restore what one button would have done, so don't build a Trash only to keep a timer.

**A setting can pass, if people find it.** 2.2.1 also accepts a way to turn the limit off, or to lengthen it at least tenfold, before people meet it, such as "Keep notifications until I close them" in the account settings. Most people never see that setting, so it suits a site that keeps timed toasts for other reasons, not as the main fix.

**A toast with nothing to act on.** "Settings saved" holds no action, and if the saved state is visible on the page, its disappearing does not take anything away. Give it time to be read anyway, and put anything people must act on, such as a failure, somewhere it stays.

**When the delete really happens.** Undo in place and the lasting toast both hold a delete open. Commit it on the person's next action or when they leave, not on a timer, and keep the server state honest: either send the delete at once and undo it with a restore call, or send it when it is committed. If people can lose work by closing the tab, the first is safer.

**Where focus goes.** When the pressed control disappears, focus must move; left alone it falls to the page body and keyboard users start again from the top. Moving focus to an Undo that replaced the row is expected. Moving focus to a toast at the edge of the page, in the middle of other work, is not: keep that for a message that the person must answer.

**Many toasts.** A stack of lasting toasts covers the page. Replace the previous toast with the next one, as the lasting toast does, or cap the stack and offer "Close all".

**Not a fix.** `role="alert"` or `aria-live="assertive"` gets the toast read sooner, but the time to act stays the same, and it interrupts whatever the screen reader was saying. A longer timer, such as ten seconds, is still a fixed limit that someone will miss.

## Verify the fix

1. Read the code: no timer removes a message that holds the only Undo, every message is written into a status region that was already on the page (or reached by focus on its action), and focus moves to an element that exists when the pressed button is removed.
2. With a screen reader on, delete a message: what happened is announced. Then wait a full minute, and the Undo can still be reached and used.
3. Using only the keyboard, delete a message and then move slowly: Tab reaches the Undo, and Enter brings the message back to where it was.
4. Zoom the page to 400% or use a screen magnifier on the list: delete a message, and the message and its Undo can be found near the list without hurrying.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the toast component or the delete flow changes.

## Limits

This covers one kind of message, the confirmation of an action that can be undone. Errors, alerts that need an answer, and notifications from other people are topics of their own, and so is making sure the announcement is heard at all (see "Status messages that are shown but never announced"). Live regions and focus behave differently across screen readers and browsers. Test your real messages, at the pace your users work.
