Skip to content

Toast notification

wu-toast is a short notification in a corner of the viewport. Toasts live in a wu-toast-region, a polite live region, so screen readers announce them without moving focus. The kit script shows them with toast(), closes them after a delay and pauses while the region is hovered or focused.

Class or attribute Effect
wu-toast-region Root, on <section aria-live="polite" aria-label>. Fixed column of toasts at the bottom end corner, never taller than the viewport (it scrolls); hidden when empty
top-start, top-end, bottom-start Region in another corner. The top corners put the newest toast on top
wu-toast Part. Raised panel with a border and a shadow
wu-toast-title Part. Semibold title
wu-toast-close Part, a <button> with aria-label. Cross drawn in CSS
brand, success, danger, warning, info on a toast Colored start edge and title
data-duration on a toast in the markup Milliseconds before it closes, 5000 by default, 0 keeps it
animated on the region Toasts slide in, fade out and stack, opt-in

Each button calls toast(). The region in the markup carries animated, so these toasts slide in and stack: hover or focus the stack to fan it out. Without animated, toasts appear and close without motion.

<section class="wu-toast-region animated" aria-live="polite" aria-label="Notifications"></section>
<div class="flex wrap gap-2">
<button class="wu-btn" type="button" data-toast="Your draft was saved.">Plain</button>
<button class="wu-btn" type="button" data-toast="Your changes are live." data-title="Saved" data-variant="success">Success</button>
<button class="wu-btn" type="button" data-toast="A new version is available." data-title="Update" data-variant="brand">Brand</button>
<button class="wu-btn" type="button" data-toast="Maintenance starts at 22:00." data-title="Heads up" data-variant="info">Info</button>
<button class="wu-btn" type="button" data-toast="Your storage is almost full." data-title="Storage" data-variant="warning">Warning</button>
<button class="wu-btn" type="button" data-toast="The upload failed. Close this message when done." data-title="Error" data-variant="danger" data-duration="0">Danger, stays open</button>
</div>

Put the region at the end of <body>, outside any element that creates a stacking context (isolation: isolate, transform, a z-index), so it stays above the page. This page moves the region above to the end of <body> for that reason.

The script behind these buttons:

import { toast } from '@wovenui/components/js';
for (const button of document.querySelectorAll('[data-toast]')) {
button.addEventListener('click', () => {
const { toast: message, title, variant, duration } = button.dataset;
toast(message, { title, variant, duration: duration && Number(duration) });
});
}
toast('Your changes are live.', { title: 'Saved', variant: 'success' });
toast('Upload failed.', { title: 'Error', variant: 'danger', duration: 0 }); // stays until closed
Option Value
title Title text, shown in wu-toast-title
variant brand, success, danger, warning or info
duration Milliseconds before it closes, 5000 by default; 0 keeps it until closed
closeLabel aria-label of the close button, Close by default

toast() returns the toast element. It adds a wu-toast-region to the end of <body> when the page has none, and otherwise uses the first one. dismiss(element) closes a toast from code.

At most three toasts stay open: a fourth closes the oldest one that closes on its own, never one with duration: 0 or one holding focus. A message with the same variant as a toast already open does not add a second toast; it restarts the open one’s timer and returns it.

A toast rendered by the server goes inside the region. The script closes it after data-duration, and the close button works.

<section class="wu-toast-region" aria-live="polite" aria-label="Notifications">
<div class="wu-toast success" data-duration="8000">
<p class="wu-toast-title">Saved</p>
<p>Your changes are live.</p>
<button class="wu-toast-close" type="button" aria-label="Close"></button>
</div>
</section>

The region sits in the bottom end corner. top-start, top-end and bottom-start move it:

<section class="wu-toast-region top-end" aria-live="polite" aria-label="Notifications"></section>

--wu-toast--Width sets the toast width, 22rem by default.

On a right-to-left page, the region’s corners follow the reading direction: the default bottom end corner is the bottom left. Inside a toast, the colored edge sits at the inline start and the close button at the inline end, so both swap sides.

تم الحفظ

تغييراتك منشورة الآن.

<div class="wu-toast success">
<p class="wu-toast-title">تم الحفظ</p>
<p>تغييراتك منشورة الآن.</p>
<button class="wu-toast-close" type="button" aria-label="إغلاق"></button>
</div>

This preview shows a toast on its own; in an app it lives in the region.

With animated on the region, older toasts stack behind the newest one and fan out while the region is hovered or focused. Toasts deeper than the third are hidden until the stack fans out, so their close buttons are not in the tab order while they cannot be seen. Under prefers-reduced-motion: no-preference, toasts also slide in from their edge, fade out before removal and glide between positions; with reduced motion the stack is the same, without the motion. wu-animated on an ancestor turns the same behavior on.

  • The region is a polite live region with a label, so screen readers announce new toasts after what they are reading. Create the region at page load: a region added together with its first toast may not be announced, which is why toast() waits a moment before inserting.
  • Toasts never take focus. The timer pauses while the pointer is over the region or focus is inside it, so there is time to read or reach the close button.
  • Closing a toast from the keyboard moves focus to the next toast’s close button, or back to where focus was before the region when no toast is left.
  • The title text carries the meaning; the colored edge only repeats it.
  • For an error that needs an answer, use duration: 0 or a confirm modal.
@use "@wovenui/components/toast";

The toast needs no other component. The demo also uses the button.