Toast stack
A stacked toast notification system with variants, auto-dismiss timers, pause on hover and a polite live region.
Tailwind CSSNotificationstoastnotificationslive-regionalerts
Live preview
Code
<!-- One container per page; toasts are appended by toast.js -->
<div id="toast-region" aria-live="polite" aria-atomic="false"
class="pointer-events-none fixed inset-x-4 bottom-4 z-50 flex flex-col items-end gap-2 sm:inset-x-auto sm:right-4 sm:w-80"></div>
<button type="button" class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
onclick="toast('Changes saved', 'success')">Try a toast</button>
const kinds = {
success: { cls: 'bg-emerald-600', delay: 3500 },
error: { cls: 'bg-red-600', delay: 7000 },
info: { cls: 'bg-slate-800', delay: 4500 },
};
const region = document.getElementById('toast-region');
function toast(message, kind = 'info') {
const { cls, delay } = kinds[kind] ?? kinds.info;
const el = document.createElement('div');
el.className = `pointer-events-auto flex w-full items-start gap-3 rounded-lg px-4 py-3 text-sm text-white shadow-lg transition-opacity duration-200 ${cls}`;
const text = document.createElement('p');
text.className = 'flex-1';
text.textContent = message; // textContent keeps input inert
const close = document.createElement('button');
close.type = 'button';
close.setAttribute('aria-label', 'Dismiss');
close.textContent = '×';
el.append(text, close);
region.append(el);
let timer;
const dismiss = () => {
clearTimeout(timer);
el.classList.add('opacity-0');
setTimeout(() => el.remove(), 200); // wait for the fade
};
const start = () => (timer = setTimeout(dismiss, delay));
el.addEventListener('mouseenter', () => clearTimeout(timer));
el.addEventListener('focusin', () => clearTimeout(timer));
el.addEventListener('mouseleave', start);
el.addEventListener('focusout', start);
close.addEventListener('click', dismiss);
start();
}
window.toast = toast;
How it works
What it solves
Apps need brief confirmation messages that do not block work. Hand-rolled toasts often disappear before they are read, steal focus, or stay invisible to screen readers. This stack avoids all of these with a small API.
How it works
A fixed container in the bottom-right holds the toasts and is marked aria-live="polite" with aria-atomic="false", so each added node is announced without interrupting. The toast(message, kind) function builds an element from a variant map of Tailwind classes, appends it, and starts a timer. Hovering or focusing a toast clears the timer and moving away restarts it, so slow readers keep control. Each toast has a close button, and removal waits for a CSS opacity transition before the node leaves the DOM. Errors use a longer delay than success messages. Message text is set with textContent, so user input cannot inject markup.
Adapting it
Call toast('Saved', 'success') after any request. Move the container to the top by changing the anchor classes. Cap the stack by removing the oldest child when the count passes a limit.
Pitfalls
- Never rely on a toast for errors the user must act on; keep those inline.
- Pausing needs both pointer and focus listeners.
- Timers must be cleared on manual close to avoid removing a detached node.
How to use it
- Paste
toasts.htmlonce in your layout, near the end of<body>. - Load
toast.jsas a module or withdeferand calltoast()from your own scripts. - Adjust the
kindsmap to add variants or change colours. - Tune the
delayvalues to fit your copy length.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.