Toast Notifcations by Artclick
by artclick
HTML
<div class="demo-panel">
<button id="btnDefault">Show toast</button>
<button id="btnSuccess" class="success">Success</button>
<button id="btnError" class="error">Error</button>
<button id="btnWarning" class="warning">Warning</button>
<button id="btnTheme" class="theme">Toggle theme</button>
</div>
<div class="toast-region" id="toastRegion" role="region" aria-label="Notifications"></div>
CSS
:root {
--toast-bg: #1f2430;
--toast-text: #ffffff;
--page-bg: #f4f5f7;
--page-text: #1a1a1a;
}
[data-theme="dark"] {
--toast-bg: #2a2f3d;
--toast-text: #e8eaed;
--page-bg: #14171c;
--page-text: #e8eaed;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
background: var(--page-bg);
color: var(--page-text);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo-panel {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
max-width: 420px;
}
.demo-panel button {
border: none;
border-radius: 0.5rem;
padding: 0.7rem 1.1rem;
font-size: 0.9rem;
cursor: pointer;
background: #2f3542;
color: #fff;
transition: transform 0.15s ease;
}
.demo-panel button:hover {
transform: translateY(-1px);
}
.demo-panel button.success { background: #1b7a4d; }
.demo-panel button.error { background: #a3312f; }
.demo-panel button.warning { background: #a6741b; }
.demo-panel button.theme { background: #444b5c; }
/* Toast region + toasts */
.toast-region {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
display: flex;
flex-direction: column-reverse;
gap: 0.6rem;
z-index: 1000;
pointer-events: none;
}
.toast {
pointer-events: auto;
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 260px;
max-width: 360px;
padding: 0.85rem 1rem;
border-radius: 0.6rem;
background: var(--toast-bg);
color: var(--toast-text);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
animation: toast-in 0.25s ease-out;
touch-action: pan-y;
}
.toast-message {
flex: 1;
margin: 0;
font-size: 0.9rem;
line-height: 1.4;
}
.toast-close {
flex-shrink: 0;
border: none;
background: transparent;
color: inherit;
opacity: 0.7;
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
...
JavaScript
const region = document.getElementById('toastRegion');
const MAX_VISIBLE = 4;
const DEFAULT_DURATION = 4000;
function showToast(message, { duration = DEFAULT_DURATION, urgent = false, variant = '' } = {}) {
const toast = document.createElement('div');
toast.className = `toast${variant ? ` toast--${variant}` : ''}`;
toast.setAttribute('role', urgent ? 'alert' : 'status');
toast.setAttribute('aria-live', urgent ? 'assertive' : 'polite');
toast.innerHTML = `
<p class="toast-message"></p>
<button class="toast-close" aria-label="Dismiss notification">×</button>
`;
// textContent, not innerHTML — keeps user-supplied text from being parsed as markup
toast.querySelector('.toast-message').textContent = message;
region.appendChild(toast);
enforceMaxVisible();
makeSwipeable(toast);
let timer = startTimer(toast, duration);
toast.addEventListener('mouseenter', () => clearTimeout(timer));
toast.addEventListener('mouseleave', () => {
timer = startTimer(toast, duration);
});
toast.querySelector('.toast-close').addEventListener('click', () => dismiss(toast));
return toast;
}
function startTimer(toast, duration) {
return setTimeout(() => dismiss(toast), duration);
}
function dismiss(toast) {
if (toast.classList.contains('toast--leaving')) return;
toast.classList.add('toast--leaving');
toast.addEventListener('animationend', () => toast.remove(), { once: true });
}
function enforceMaxVisible() {
const toasts = region.querySelectorAll('.toast:not(.toast--leaving)');
if (toasts.length > MAX_VISIBLE) {
dismiss(toasts[0]);
}
}
function makeSwipeable(toast) {
let startX = 0;
let currentX = 0;
let dragging = false;
toast.addEventListener('pointerdown', (e) => {
dragging = true;
startX = e.clientX;
toast.style.transition = 'none';
});
toast.addEventListener('pointermove', (e) => {
if (!dragging) return;
currentX =...