Toast Stack
Small notifications spring in at the corner of the screen, stack neatly, show a shrinking timer bar and slide away on their own. Hover one to keep it open.
How to use it
- Paste the snippet just before
</head>. - Add
data-toast="Your message"to any button. Clicking it shows the toast.
Options
Add any of these attributes to the same element.
data-toast | The message to show. |
data-toast-duration | How long it stays. Default 3200 ms. |
data-toast-container | On a wrapping element: show toasts inside it instead of the page corner. |
From code: animacious.toast('Saved', { duration: 4000 }).
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Toast Stack: small notifications spring in at the corner of the screen,
// stack neatly, show a shrinking timer bar and slide away on their own.
// Usage: <button data-toast="Link copied">Copy link</button>
// or from code: animacious.toast('Saved', { duration: 4000 })
// Optional on the button: data-toast-duration (ms, default 3200).
// Contained: add data-toast-container to an element to show toasts inside it
// (pass { container: element } from code).
var DEFAULTS = { duration: 3200, max: 4 };
var css =
'.ts-stack{position:fixed;right:20px;bottom:20px;z-index:2147483000;display:flex;flex-direction:column;align-items:flex-end;gap:0;pointer-events:none;font:500 14px/1.4 system-ui,sans-serif}' +
'.ts-stack.is-contained{position:absolute;right:12px;bottom:12px}' +
'.ts-wrap{overflow:hidden}' +
'.ts-toast{position:relative;margin-top:10px;padding:12px 16px 14px;border-radius:12px;background:#111114;color:#fff;box-shadow:0 10px 30px rgba(0,0,0,.18);pointer-events:auto;overflow:hidden;max-width:320px}' +
'.ts-bar{position:absolute;left:0;bottom:0;height:3px;width:100%;background:#a855f7;transform-origin:0 50%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var stacks = new Map();
function stackFor(container) {
var key = container || document.body;
if (stacks.has(key)) return stacks.get(key);
var s = document.createElement('div');
s.className = 'ts-stack' + (container ? ' is-contained' : '');
s.setAttribute('role', 'status');
s.setAttribute('aria-live', 'polite');
if (container && getComputedStyle(container).position === 'static') container.style.position = 'relative';
key.appendChild(s);
stacks.set(key, s);
return s;
}
function dismiss(wrap) {
if (wrap._gone) return;
wrap._gone = true;
var toast = wrap.firstChild;
anime.animate(toast, { x: '115%', opacity: 0, duration: 350, ease: 'in(3)' });
anime.animate(wrap, {
height: '0px',
duration: 300,
delay: 220,
ease: 'inOut(2)',
onComplete: function () { wrap.remove(); }
});
}
function toast(message, opts) {
opts = opts || {};
var stack = stackFor(opts.container);
var duration = opts.duration || DEFAULTS.duration;
var wrap = document.createElement('div');
wrap.className = 'ts-wrap';
var t = document.createElement('div');
t.className = 'ts-toast';
t.textContent = message;
var bar = document.createElement('div');
bar.className = 'ts-bar';
t.appendChild(bar);
wrap.appendChild(t);
stack.appendChild(wrap);
// Too many? Retire the oldest.
var live = Array.from(stack.children).filter(function (w) { return !w._gone; });
if (live.length > DEFAULTS.max) dismiss(live[0]);
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setTimeout(function () { wrap.remove(); }, duration);
return;
}
// Open a gap for it (pushing older toasts up), then spring it in.
var h = wrap.offsetHeight;
anime.animate(wrap, { height: ['0px', h + 'px'], duration: 300, ease: 'out(3)' });
anime.animate(t, { x: ['110%', '0%'], duration: 700, ease: 'outBack(1.4)' });
var timer = anime.animate(bar, { scaleX: [1, 0], duration: duration, ease: 'linear', onComplete: function () { dismiss(wrap); } });
t.addEventListener('mouseenter', function () { timer.pause(); });
t.addEventListener('mouseleave', function () { timer.play(); });
t.addEventListener('click', function () { dismiss(wrap); });
}
document.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('[data-toast]');
if (!b) return;
var container = b.closest('[data-toast-container]');
toast(b.getAttribute('data-toast'), { duration: +b.dataset.toastDuration || undefined, container: container || undefined });
});
window.animacious = window.animacious || {};
window.animacious.toast = toast;
})();
</script><button data-toast="Link copied">Copy link</button>