Accessibility pattern · Feedback and status

Toast notifications

Each toast is written into one role=status element that was in the page from the start, so it is announced once, politely, while focus stays where it was. Timed toasts pause while they are pointed at, focused or in a background tab, a setting turns the timer off, and a toast with an action stays until it is handled.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Q3 sales report

Shared with Finance

Send a notification

    Summary

    Revenue grew 18% on the last quarter, led by online orders from Pune and Jaipur. Returns fell to 3.2%.

    Next quarter we plan to open two stores and move all deliveries to one courier partner.

    Highlights

    • Online orders up 27%
    • Average delivery time down to 2.4 days
    • Repeat customers at 41%

    The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

    Keyboard

    Keys it answers to

    Every action works without a pointer.

    KeyWhat it does
    Enter or SpaceOn Save draft, Upload invoice or Go offline, shows a notification. Focus stays on the button you pressed.
    Tab or ShiftTabReaches the open notifications' Retry and Dismiss buttons after the page's own controls; a toast never takes focus by itself.
    EscapeIn a notification, closes it and returns focus to where you were before you moved into the stack.
    Enter or SpaceOn Retry, reconnects: Connection lost is replaced by Back online, and focus goes back to where you were.

    Screen readers

    What it announces

    Written from the roles, names and states in the markup.

    WhenExpected announcement
    Save draft is pressedSaved. Q3 sales report is up to date.
    Go offline is pressedConnection lost. Your edits stay on this device until you are back online.
    Tab reaches the stackNotifications, region. Retry, button
    Retry succeedsBack online. Your edits are synced.
    A toast closes, on its own or by DismissNothing: closing is silent

    These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

    Code

    Copy the code

    The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

    The markup

    <div class="ap-toast" data-ap-toast>
      <div class="ap-toast__app">
        <div class="ap-toast__top">
          <span class="ap-toast__doc" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg>
          </span>
          <div>
            <h3 class="ap-toast__file">Q3 sales report</h3>
            <p class="ap-toast__meta">Shared with Finance</p>
          </div>
        </div>
    
        <div class="ap-toast__try">
          <p class="ap-toast__k" id="toast-try">Send a notification</p>
          <div class="ap-toast__btns" role="group" aria-labelledby="toast-try">
            <button type="button" class="ap-btn" data-ap-make="saved">
              <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5Z"/><path d="M8 4v5h7V4"/><path d="M8 20v-6h8v6"/></svg>
              Save draft
            </button>
            <button type="button" class="ap-btn" data-ap-make="upload">
              <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>
              Upload invoice
            </button>
            <button type="button" class="ap-btn" data-ap-make="offline">
              <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2 8.8a15 15 0 0 1 4.2-2.6"/><path d="M10.7 5.1A15 15 0 0 1 22 8.8"/><path d="M5 12.5a10 10 0 0 1 3.4-2"/><path d="M16.2 10.8a10 10 0 0 1 2.8 1.7"/><path d="M8.5 16.4a5 5 0 0 1 7 0"/><path d="M12 20h.01"/><path d="m3 3 18 18"/></svg>
              Go offline
            </button>
          </div>
          <label class="ap-toast__pref">
            <input class="ap-toast__check" type="checkbox" data-ap-keep />
            <span>Don't auto-dismiss notifications</span>
          </label>
        </div>
    
        <section class="ap-toast__region" aria-label="Notifications">
          <ol class="ap-toast__list" data-ap-list></ol>
          <p class="ap-toast__sr" role="status" data-ap-say></p>
        </section>
    
        <div class="ap-toast__page">
          <h4 class="ap-toast__h">Summary</h4>
          <p>Revenue grew 18% on the last quarter, led by online orders from Pune and Jaipur. Returns fell to 3.2%.</p>
          <p>Next quarter we plan to open two stores and move all deliveries to one courier partner.</p>
          <h4 class="ap-toast__h">Highlights</h4>
          <ul class="ap-toast__points">
            <li>Online orders up 27%</li>
            <li>Average delivery time down to 2.4 days</li>
            <li>Repeat customers at 41%</li>
          </ul>
        </div>
      </div>
    
    
      <template data-ap-kind="saved">
        <li class="ap-toast__item ap-toast__item--success">
          <span class="ap-toast__mark" aria-hidden="true"><svg class="ap-toast__icon" viewBox="0 0 24 24" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16.2 9.6"/></svg></span>
          <div class="ap-toast__body">
            <p class="ap-toast__name">Saved</p>
            <p class="ap-toast__text">Q3 sales report is up to date.</p>
          </div>
          <button type="button" class="ap-toast__x" data-ap-close>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
            <span class="ap-toast__sr">Dismiss Saved</span>
          </button>
          <span class="ap-toast__timer" aria-hidden="true"></span>
        </li>
      </template>
      <template data-ap-kind="upload">
        <li class="ap-toast__item ap-toast__item--info">
          <span class="ap-toast__mark" aria-hidden="true"><svg class="ap-toast__icon" viewBox="0 0 24 24" focusable="false"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/><path d="m9 14 2 2 4-4"/></svg></span>
          <div class="ap-toast__body">
            <p class="ap-toast__name">Upload complete</p>
            <p class="ap-toast__text"><span translate="no">invoice-2041.pdf</span> is in Documents.</p>
          </div>
          <button type="button" class="ap-toast__x" data-ap-close>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
            <span class="ap-toast__sr">Dismiss Upload complete</span>
          </button>
          <span class="ap-toast__timer" aria-hidden="true"></span>
        </li>
      </template>
      <template data-ap-kind="offline">
        <li class="ap-toast__item ap-toast__item--error" data-ap-sticky>
          <span class="ap-toast__mark" aria-hidden="true"><svg class="ap-toast__icon" viewBox="0 0 24 24" focusable="false"><path d="M2 8.8a15 15 0 0 1 4.2-2.6"/><path d="M10.7 5.1A15 15 0 0 1 22 8.8"/><path d="M5 12.5a10 10 0 0 1 3.4-2"/><path d="M16.2 10.8a10 10 0 0 1 2.8 1.7"/><path d="M8.5 16.4a5 5 0 0 1 7 0"/><path d="M12 20h.01"/><path d="m3 3 18 18"/></svg></span>
          <div class="ap-toast__body">
            <p class="ap-toast__name">Connection lost</p>
            <p class="ap-toast__text">Your edits stay on this device until you are back online.</p>
            <div class="ap-toast__acts">
              <button type="button" class="ap-toast__act" data-ap-retry>Retry</button>
            </div>
          </div>
          <button type="button" class="ap-toast__x" data-ap-close>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
            <span class="ap-toast__sr">Dismiss Connection lost</span>
          </button>
        </li>
      </template>
      <template data-ap-kind="online">
        <li class="ap-toast__item ap-toast__item--success">
          <span class="ap-toast__mark" aria-hidden="true"><svg class="ap-toast__icon" viewBox="0 0 24 24" focusable="false"><path d="M2 8.8a15 15 0 0 1 20 0"/><path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8.5 16.4a5 5 0 0 1 7 0"/><path d="M12 20h.01"/></svg></span>
          <div class="ap-toast__body">
            <p class="ap-toast__name">Back online</p>
            <p class="ap-toast__text">Your edits are synced.</p>
          </div>
          <button type="button" class="ap-toast__x" data-ap-close>
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
            <span class="ap-toast__sr">Dismiss Back online</span>
          </button>
          <span class="ap-toast__timer" aria-hidden="true"></span>
        </li>
      </template>
    </div>
    

    WCAG 2.2

    What it meets

    The success criteria this pattern takes care of, and how.

    • 1.3.1 Info and Relationships Level A

      The toasts are a list inside a region named Notifications, so screen reader users can jump to them and hear how many there are.

    • 2.1.1 Keyboard Level A

      Retry, Dismiss and Escape all work from the keyboard; nothing depends on hovering to stop a timer.

    • 2.2.1 Timing Adjustable Level A

      Timed toasts pause on hover, on focus and in a background tab, a setting turns the timer off, and a toast with an action never times out.

    • 2.4.3 Focus Order Level A

      No toast takes focus. When the focused one closes, focus returns to where it was before, never to the top of the page.

    • 2.4.11 Focus Not Obscured (Minimum) Level AA

      The stack sits in the corner beneath the controls, so it does not cover the button or field that has focus.

    • 4.1.2 Name, Role, Value Level A

      Each Dismiss button is named with its toast, such as Dismiss Connection lost, so a row of them still makes sense.

    • 4.1.3 Status Messages Level AA

      Every toast is announced from a polite role=status element, without moving focus.

    Usage

    When to use it

    Use it

    • Confirming something worked that needs no answer: saved, sent, copied, uploaded.
    • Events in the background people should know about without stopping, like a lost connection.

    Use something else

    • Errors that block the task: use an alert banner near the problem, or an alert dialog.
    • Anything people will need later: a toast goes away, so keep the information on the page too.
    • Messages about one field: show them next to the field.

    Common failures

    How it usually goes wrong

    • A toast that grabs focus

      Moving focus to every notification drags keyboard and screen reader users away from their work. These are announced; Tab reaches them when wanted.

    • An action that times out

      A Retry button that leaves after five seconds can go before a screen reader user hears it or a keyboard user reaches it. A toast with an action stays until it is handled.

    • A timer nobody can stop

      Auto-dismiss is a time limit. Here it pauses on hover, on focus and in a background tab, and a setting turns it off altogether.

    • A live region made with the toast

      A new role=status element added with the message already in it is often not announced. The status element here is in the page from the start, and only its text changes.

    • The whole stack read again

      With the list itself live, some screen readers read the Dismiss and Retry buttons with each message, or read older toasts again. One status element says each message once.

    • Toasts that pile up

      An endless stack covers the page. Three show at most; the oldest timed toast makes way, and a repeat replaces its twin.

    Notes

    Building it

    • The visible stack is an ordered list in a section named Notifications, a region landmark, so screen reader users can find Retry with landmark navigation. The words are announced from a separate role=status element inside it.
    • A live region must be in the accessibility tree before its content changes: browsers report changes to regions they already know about. A region added with its text, or shown from display: none at the same moment, is often silent, so the status element is in the HTML, empty.
    • Each message is copied into the status element after emptying it, so the same toast twice is announced twice, and emptied again after seven seconds so a stale copy does not linger for people reading the region.
    • The countdown pauses rather than restarts, and the bar that shows it is decoration: hidden from assistive technology, and switched off with the slide-in for reduced motion.
    • Keep the stack clear of where people work: a toast over a focused field or a sticky footer hides it, which fails 2.4.11.

    Sources: Understanding SC 2.2.1: Timing Adjustable · Understanding SC 4.1.3: Status Messages · WAI-ARIA 1.2: the status role

    Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction