Zoeken...  ⌘K GitHub

Toast UI Elements

Toast notificaties via window.toast(). 4 varianten (success, error, warning, info). Automatisch verdwijnen.

/toast
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