---
title: 'Notification Banners and Toast Notifications'
source: 'https://academia.sh/en/courses/accessible-patterns/banners-and-toast-notifications'
course: 'Accessible Component Patterns'
language: en
updated: '2026-08-19T05:19:52+00:00'
license: 'CC BY-SA 4.0'
---

# Notification Banners and Toast Notifications

The distinction between the notification banner and the toast notification, computing how long a notification stays visible from the text's reading speed, coalescing events that arrive one after another, and measuring loss in the polite–assertive queue.

The user opened the tooltip: they turned toward an element and the layer appeared. A
notification has no such link. The system sends word of something on its own; the user is
looking wherever they happen to be looking on the screen, and might not be looking at the
screen at all.

This raises three new questions. How long should a notification stay on screen — text
that disappears counts as unread. What should happen with two notifications that arrive
at the same time — if one suppresses the other, what is lost? And how does a user outside
the visual channel receive the notification at all? This lesson ties all three to a
computable constraint.

## What It Solves, When Not to Use It

There are two separate patterns, and confusing them is the most common defect.

A **notification banner** is a section that stays inside the page's flow and remains
until the user or the system removes it. It stays as long as its content remains valid:
"Three of your records are overdue", "The catalog is read-only for maintenance". Because
it is persistent, it can hold an action and focus can reach inside it.

A **toast** is a layer that appears above the flow and disappears on its own once its time
runs out. It announces only a **completed** and **reversible** action: "Record borrowed".

Situations where it should not be used all derive from a single rule: **a layer that
disappears cannot carry anything the user is required to do.**

- **An error requiring recovery** does not belong in a toast; if the user is not looking
  at the screen, they never see the error at all, and the system fails silently.
- **A question requiring confirmation** does not belong in a toast; once time runs out,
  the question goes unanswered.
- **Information that must be kept on record** does not belong in a toast; the
  notification also needs a counterpart in a persistent list.

Can a toast hold an action? It can, under exactly one condition: the action must be
**optional**. "Undo" fits this — if the user does nothing, the result is still
consistent. "Retry" does not — if nothing is done, the request is lost.

## Native Element First, ARIA Second

There is no native element set aside for notifications; the setup is built with roles.

| Part | Source |
|---|---|
| Polite notification | `role="status"` (implicit `aria-live="polite"`) |
| Assertive notification | `role="alert"` (implicit `aria-live="assertive"`) |
| Accumulating record | `role="log"` — an ordered, non-expiring notification list |
| The banner's name | if the banner is a section, bound to its heading with `aria-labelledby` |

The rule set out in the Screen Reader Experience lesson is a setup requirement here: **the
live region's container must already exist in the document.** Adding the container
together with its content when the notification arrives can result in the notification
never being announced. The correct setup places an empty container when the page is built
and writes only the text into it.

The second rule is choosing a priority. Polite priority joins a queue; assertive priority
drops whatever is waiting. Borrow confirmation is polite; an error that stops the user's
work is assertive. Making every notification assertive strips the priority of its meaning
and produces the loss the measurement below shows.

## Duration Is Computed from the Text

A toast's duration is not a design preference; it is a quantity derived from the length of
the text. The model sums three allowances: attention turning toward the notification,
reading the text, and — if there is an action — reaching the action and deciding. The
result is clamped to a lower and an upper bound.

```js
// notification.mjs — computing visible duration from text, coalescing, and the announcement queue

// Model values (chosen parameters): reaction allowance, reading speed, action allowance.
const REACTION = 700;       // ms — attention turning toward the notification
const READING_SPEED = 200;  // words/minute — the value chosen for reading from a screen
const ACTION = 1500;        // ms — allowance for reaching and deciding on the notification's action
const MIN_MS = 4000, MAX_MS = 10000;

const wordCount = (m) => m.trim().split(/\s+/).length;
function duration(m, hasAction) {
  const raw = REACTION + (wordCount(m) / READING_SPEED) * 60000 + (hasAction ? ACTION : 0);
  return { raw, visible: Math.min(MAX_MS, Math.max(MIN_MS, Math.round(raw / 100) * 100)) };
}

const EXAMPLES = [
  { m: "Record borrowed.", action: false },
  { m: "Record borrowed. Return date is August 11.", action: false },
  { m: "Record borrowed. Return date is August 11.", action: true },
  { m: "Filter updated; 128 records in the list.", action: false },
  { m: "The borrow request could not be sent. The connection dropped; use the retry button.", action: true },
];

console.log("notification text                                          words  action  raw duration  visible");
for (const o of EXAMPLES) {
  const s = duration(o.m, o.action);
  const short = o.m.length > 55 ? o.m.slice(0, 52) + "..." : o.m;
  console.log(
    `${short.padEnd(58)} ${String(wordCount(o.m)).padStart(5)}  ${(o.action ? "yes" : "no").padStart(6)}  ` +
      `${(s.raw.toFixed(0) + " ms").padStart(12)}  ${(s.visible + " ms").padStart(7)}`,
  );
}

// --- Consecutive events: coalesced and uncoalesced ----------------------------
const EVENTS = [
  { at: 0,    type: "borrow", subject: "Light and Shadow" },
  { at: 350,  type: "borrow", subject: "Handbook of Network Protocols" },
  { at: 900,  type: "borrow", subject: "North Slope Measurements" },
  { at: 4200, type: "filter", subject: "128" },
  { at: 4600, type: "filter", subject: "42" },
  { at: 5100, type: "filter", subject: "7" },
  { at: 7000, type: "error",  subject: "North Slope Measurements" },
];

const COALESCE_WINDOW = 1500;   // ms — the interval in which events of the same type are gathered
const PRIORITY = { borrow: "polite", filter: "polite", error: "assertive" };
const HAS_ACTION = { borrow: true, filter: false, error: true };

const writeText = (type, subjects) => {
  if (type === "borrow")
    return subjects.length === 1 ? `${subjects[0]} borrowed.` : `${subjects.length} records borrowed.`;
  if (type === "filter") return `Filter updated; ${subjects[subjects.length - 1]} records in the list.`;
  return `${subjects[0]} could not be borrowed. The request was declined because no copy remains.`;
};

function notifications(events, coalesce) {
  if (!coalesce)
    return events.map((o) => ({ at: o.at, type: o.type, text: writeText(o.type, [o.subject]) }));
  const groups = [];
  for (const o of events) {
    const last = groups[groups.length - 1];
    if (last && last.type === o.type && o.at - last.lastAt <= COALESCE_WINDOW) {
      last.subjects.push(o.subject);
      last.lastAt = o.at;
    } else groups.push({ type: o.type, subjects: [o.subject], lastAt: o.at });
  }
  // A coalesced group is written once its window closes.
  return groups.map((g) => ({ at: g.lastAt, type: g.type, text: writeText(g.type, g.subjects) }));
}

function queue(list) {
  const incoming = [...list].sort((a, b) => a.at - b.at);
  let i = 0, active = null, waiting = [];
  let dropped = 0, cutShort = 0, complete = 0;
  const log = [];

  const start = (b, at) => {
    const s = duration(b.text, HAS_ACTION[b.type]).visible;
    active = { ...b, start: at, end: at + s, duration: s };
    log.push({ at, priority: PRIORITY[b.type], wait: at - b.at, text: b.text, duration: s });
  };

  while (i < incoming.length || waiting.length || active) {
    const nextEvent = i < incoming.length ? incoming[i].at : Infinity;
    const end = active ? active.end : Infinity;
    if (end <= nextEvent) {
      complete++;
      active = null;
      if (waiting.length) start(waiting.shift(), end);
      continue;
    }
    if (nextEvent === Infinity) break;
    const b = incoming[i++];
    if (PRIORITY[b.type] === "assertive") {
      dropped += waiting.length;
      waiting = [];
      if (active) { cutShort++; log[log.length - 1].cut = b.at - active.start; }
      start(b, b.at);
    } else if (!active) start(b, b.at);
    else waiting.push(b);
  }
  return { dropped, cutShort, complete, log };
}

for (const coalesce of [false, true]) {
  const list = notifications(EVENTS, coalesce);
  const s = queue(list);
  console.log(`\n${coalesce ? "coalesced" : "each event its own notification"}: ` +
    `${EVENTS.length} events -> ${list.length} notifications -> ${s.log.length} displays`);
  console.log("    at  priority  wait    visible  text");
  for (const k of s.log) {
    const visible = k.cut === undefined ? `${k.duration} ms` : `${k.cut}/${k.duration}`;
    console.log(`  ${String(k.at).padStart(4)}  ${k.priority.padEnd(9)} ${(k.wait ? k.wait + " ms" : "—").padStart(8)} ` +
      `${visible.padStart(10)}  ${k.text}`);
  }
  console.log(`  never displayed: ${s.dropped}, cut short: ${s.cutShort}, fully displayed: ${s.complete}`);
}
```

```
notification text                                          words  action  raw duration  visible
Record borrowed.                                               2      no       1300 ms  4000 ms
Record borrowed. Return date is August 11.                     7      no       2800 ms  4000 ms
Record borrowed. Return date is August 11.                     7     yes       4300 ms  4300 ms
Filter updated; 128 records in the list.                       7      no       2800 ms  4000 ms
The borrow request could not be sent. The connection...       14     yes       6400 ms  6400 ms

each event its own notification: 7 events -> 7 notifications -> 3 displays
    at  priority  wait    visible  text
     0  polite           —    4000 ms  Light and Shadow borrowed.
  4000  polite     3650 ms  3000/4000  Handbook of Network Protocols borrowed.
  7000  assertive        —    6700 ms  North Slope Measurements could not be borrowed. The request was declined because no copy remains.
  never displayed: 4, cut short: 1, fully displayed: 2

coalesced: 7 events -> 3 notifications -> 3 displays
    at  priority  wait    visible  text
   900  polite           —    4000 ms  3 records borrowed.
  5100  polite           —  1900/4000  Filter updated; 7 records in the list.
  7000  assertive        —    6700 ms  North Slope Measurements could not be borrowed. The request was declined because no copy remains.
  never displayed: 0, cut short: 1, fully displayed: 2
```

The first table ties duration to the text. A two-word notification's raw duration is 1.3
seconds and gets pulled up to the floor; the fourteen-word error message calls for 6.4
seconds. The floor keeps short notifications from flashing past unread, and the ceiling
keeps long text from becoming permanent on screen. The third row shows the action
allowance: the same text rises to 4.3 seconds when it carries an "Undo" button, because
reading is not enough — the user also has to decide.

A notification that leans on the ceiling is a signal: the text is too long for a
notification and belongs in a banner or a persistent list.

The second and third blocks process the same seven events with two different setups. **In
the uncoalesced setup, only three of the seven events reach the screen**; four are never
displayed, and one is cut short. Worse is the second row: the user sees the notification
for the second record 3.65 seconds after borrowing it. When a notification arrives this
long after the action it reports, the user mistakes it for the result of a third action.

**In the coalesced setup, the seven events come down to three notifications and nothing
is lost.** Two rules achieve this. Events of the same type are gathered across a window
and turn into a single, counted text: "3 records borrowed" instead of three separate
"borrowed" notifications. For consecutive filter changes, **only the final state** is
written; intermediate values are not announced, because the final number is what matters
to the user.

In both setups, the assertive error notification cuts off the active one. This follows
directly from the definition of priority and is not a defect to fix; what needs fixing is
making sure the information that was cut short exists somewhere else. If the cut-short
notification's counterpart stands in a persistent record list, the loss stops being real.

## Measurable Constraints

**2.2.1 Timing Adjustable.** A notification that disappears on its own is a time limit.
The criterion requires that the limit can be turned off, adjusted, or extended. In
practice this comes down to three rules: the timer stops when the notification is hovered
over or gains focus; the notification can be dismissed by hand; and its content also
exists in a persistent list.

**4.1.3 Status Messages.** The notification must be announced without moving focus. Code
that moves focus to the notification violates the criterion from the opposite direction:
the user is pulled away from what they were doing.

**2.2.2 Pause, Stop, Hide.** If the notification slides in from somewhere, that motion
must last less than five seconds or be able to be stopped. The same criterion also covers
a notification log accumulating on screen that advances on its own.

**1.4.13.** If a notification banner is positioned above the page and covers the content
beneath it, the three conditions from the previous lesson apply here too, dismissibility
above all.

**Position and scrolling.** A toast appears in a corner of the screen and covers the
content in that corner. If the covered content is a focused element, the 2.4.11 criterion
is violated. Space set aside beneath the notification area is not a design flourish; it is
what this criterion requires.

## Common Mistakes and How to Recognize Them

**One notification per event.** In a bulk operation, notifications pile up on top of each
other. How to recognize it: borrow several records quickly and count how many
notifications appear on screen; if the count equals the number of actions, there is no
coalescing.

**An error inside a toast.** If the user is not looking at the screen when the request
fails, no trace remains at all. How to recognize it: produce the error and wait; if no
trace of it remains on screen once the time runs out, the wrong pattern was chosen.

**The notification container is added afterward.** The announcement works sometimes and
not other times. How to recognize it: check whether the live region container is in the
document when the page is first built.

**Fixed duration.** If every notification stays for the same length of time, long text
goes unread. How to recognize it: measure the longest notification text by word count and
compare it against the duration the model gives.

## Summary

- A notification banner stays inside the flow and is persistent; a toast appears above the
  flow and disappears on its own.
- A layer that disappears cannot carry anything the user is required to do; a toast can
  hold only an optional action.
- The live region container exists in the document beforehand; polite priority joins a
  queue and assertive priority drops what is waiting, and this distinction loses its
  meaning in a setup that makes every notification assertive.
- Visible duration is computed from the text's word count, the reaction allowance, and the
  action allowance; it is clamped to a floor and a ceiling, and text that leans on the
  ceiling belongs to a banner rather than a notification.
- Consecutive events of the same type are coalesced within a window; countable events
  reduce to a single counted text, and consecutive state changes reduce to only the final
  state.
- Disappearing on its own is a time limit: it must stop on hover, must be dismissible by
  hand, and its content must also exist in a persistent list.

## Next Step

The notification said that a task **finished**. What appears on screen during the
interval that the same task runs is a separate pattern. A waiting indicator splits into
two basic forms: an indicator that reports a task of unknown duration is running, and an
indicator that shows the progress of a task whose completion ratio can be computed. The
two differ in role, announcement, and update frequency — and for the second, how the
percentage is computed and how the remaining time is estimated determines the user's
decision to wait or not. The next lesson writes both indicators as a specification.
