Toast UI Elements
Toast notificaties via window.toast(). 4 varianten (success, error, warning, info). Automatisch verdwijnen.
src/components/ui/Toast.astro
---
/**
* Toast / Notification
* Stacked toast container + individuele toast.
* Varianten: success, error, warning, info.
* JS API: window.toast('message', { variant, duration })
*/
interface Props {
position?: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
}
const { position = 'bottom-right' } = Astro.props;
---
<div
class:list={['toast-container', `toast-container--${position}`]}
id="toast-container"
data-component="toast"
aria-live="polite"
aria-label="Notificaties"
></div>
<script>
const icons: Record<string, string> = {
success: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>',
error: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>',
warning: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>',
info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>',
};
interface ToastOptions {
variant?: 'success' | 'error' | 'warning' | 'info';
duration?: number;
description?: string;
}
function showToast(message: string, options: ToastOptions = {}) {
const { variant = 'info', duration = 4000, description } = options;
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast--${variant}`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<span class="toast__icon">${icons[variant]}</span>
<div class="toast__content">
<span class="toast__message">${message}</span>
${description ? `<span class="toast__desc">${description}</span>` : ''}
</div>
<button class="toast__close" aria-label="Sluiten">
<svg width="14" height="14" viewBox="0 0 18 18" fill="none">
<path d="M4 4l10 10M14 4L4 14" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
</svg>
</button>
<div class="toast__progress"></div>
`;
// Animate in
requestAnimationFrame(() => {
container.prepend(toast);
requestAnimationFrame(() => toast.classList.add('toast--visible'));
});
function dismiss() {
toast.classList.remove('toast--visible');
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
}
toast.querySelector('.toast__close')?.addEventListener('click', dismiss);
const timer = setTimeout(dismiss, duration);
// Progress bar
const progress = toast.querySelector<HTMLElement>('.toast__progress');
if (progress) {
progress.style.animationDuration = `${duration}ms`;
progress.classList.add('toast__progress--running');
}
toast.addEventListener('mouseenter', () => clearTimeout(timer));
}
// Global API
(window as any).toast = showToast;
</script>
<style>
/* Container */
.toast-container {
position: fixed;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.625rem;
padding: 1rem;
pointer-events: none;
max-width: 400px;
width: calc(100vw - 2rem);
}
.toast-container--top-right { top: 0; right: 0; }
.toast-container--top-left { top: 0; left: 0; }
.toast-container--top-center { top: 0; left: 50%; transform: translateX(-50%); }
.toast-container--bottom-right { bottom: 0; right: 0; }
.toast-container--bottom-left { bottom: 0; left: 0; }
.toast-container--bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); }
/* Toast */
.toast {
display: flex;
align-items: flex-start;
gap: 0.75rem;
background: var(--color-primary, #0a0a0a);
color: #fff;
padding: 1rem 1rem 1rem 1rem;
border-radius: var(--radius, 0.5rem);
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
pointer-events: auto;
position: relative;
overflow: hidden;
opacity: 0;
transform: translateY(8px) scale(0.97);
transition: opacity 0.25s cubic-bezier(0.22,1,0.36,1), transform 0.25s cubic-bezier(0.22,1,0.36,1);
}
.toast--visible {
opacity: 1;
transform: translateY(0) scale(1);
}
/* Variants */
.toast--success { background: #166534; border-left: 3px solid #22c55e; }
.toast--error { background: #7f1d1d; border-left: 3px solid #ef4444; }
.toast--warning { background: #78350f; border-left: 3px solid #f59e0b; }
.toast--info { background: var(--color-primary, #0a0a0a); border-left: 3px solid var(--color-accent, #6366f1); }
.toast__icon {
flex-shrink: 0;
margin-top: 0.125rem;
display: flex;
align-items: center;
color: rgba(255,255,255,0.8);
}
.toast__content { flex: 1; min-width: 0; }
.toast__message {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: #fff;
line-height: 1.4;
}
.toast__desc {
display: block;
font-size: 0.8125rem;
color: rgba(255,255,255,0.6);
margin-top: 0.125rem;
line-height: 1.4;
}
.toast__close {
flex-shrink: 0;
background: none;
border: none;
color: rgba(255,255,255,0.5);
cursor: pointer;
padding: 0.125rem;
display: flex;
align-items: center;
border-radius: 4px;
transition: color 0.15s;
margin-top: 0.125rem;
}
.toast__close:hover { color: rgba(255,255,255,0.9); }
/* Progress bar */
.toast__progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: rgba(255,255,255,0.25);
width: 100%;
transform-origin: left;
}
.toast__progress--running {
animation: toast-progress linear forwards;
}
@keyframes toast-progress {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
position | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center' | 'bottom-right' | Positie op het scherm |
— | JS API | — | window.toast(message, { variant, duration, title }) |
* = verplicht