Zum Hauptinhalt springen

Ephemeral feedback messages that appear in response to user actions. Distinct from the notification center (persistent) - toasts are immediate, temporary, and action-specific.


When to Use

Use Toast Use Notification Center
Form saved successfully New comment on your post
Item deleted System maintenance scheduled
Settings updated Security alert
Error during action Export ready for download
Copied to clipboard Someone mentioned you

Rule: Toasts are for feedback on user-initiated actions. Notifications are for events that happen independently.


Toast Types

Type Icon Use Case Auto-dismiss
success i-lucide-check-circle Action completed 4s
error i-lucide-x-circle Action failed Manual
warning i-lucide-alert-triangle Action succeeded with caveats 6s
info i-lucide-info Neutral information 4s

Wireframe

                                    ┌────────────────────────────────┐
                                    │ ✓ Settings saved               │
                                    │                          [✕]  │
                                    └────────────────────────────────┘

                                    ┌────────────────────────────────┐
                                    │ ✗ Failed to delete item        │
                                    │   Network error. Try again?    │
                                    │                   [Retry] [✕]  │
                                    └────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│                                                                      │
│   Sidebar        Main Content                                        │
│                                                                      │
│                                                                      │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Position: Top-right corner, below any sticky headers. Stacks vertically with newest on top.


Implementation

Toast Store

SSR-safe context factory. The root layout creates the instance once with setToastContext(); child components read it with getToast().

// src/lib/state/toast.svelte.ts
import { getContext, setContext } from 'svelte';

type ToastType = 'success' | 'error' | 'warning' | 'info';

interface ToastAction {
  label: string;
  onclick: () => void;
}

interface Toast {
  id: string;
  type: ToastType;
  message: string;
  duration: number; // 0 = persistent until dismissed
  action?: ToastAction;
}

const TOAST_CTX = Symbol('toast');

export function createToastState() {
  let toasts = $state<Toast[]>([]);

  function add(type: ToastType, message: string, duration = 5000, action?: ToastAction) {
    const id = crypto.randomUUID();
    toasts.push({ id, type, message, duration, action });

    if (duration > 0) {
      setTimeout(() => remove(id), duration);
    }
  }

  function remove(id: string) {
    toasts = toasts.filter((t) => t.id !== id);
  }

  return {
    get items() {
      return toasts;
    },
    success: (msg: string, duration?: number) => add('success', msg, duration),
    error: (msg: string, duration?: number) => add('error', msg, duration),
    warning: (msg: string, duration?: number) => add('warning', msg, duration),
    info: (msg: string, duration?: number) => add('info', msg, duration),
    // Full-control entry point — the only way to attach an action button.
    show: ({ type = 'info', message, duration = 5000, action }: { type?: ToastType; message: string; duration?: number; action?: ToastAction }) =>
      add(type, message, duration, action),
    remove,
  };
}

// Call in root layout.
export function setToastContext() {
  const toast = createToastState();
  setContext(TOAST_CTX, toast);
  return toast;
}

// Call in child components.
export function getToast() {
  return getContext<ReturnType<typeof createToastState>>(TOAST_CTX);
}

The API is { items, success, error, warning, info, show, remove }. Each toast carries a message string and an optional action ({ label, onclick }, rendered as a project Button). success/error/warning/info are convenience wrappers with no action support; show({ type?, message, duration?, action? }) is the general entry point — pass duration: 0 to keep the toast until the user dismisses it or the action fires.

Toast Container

<!-- src/lib/components/composites/toast/ToastContainer.svelte -->
<script lang="ts">
  import { Button } from '$lib/components/primitives';
  import { getToast } from '$lib/state/toast.svelte';
  import { fly } from 'svelte/transition';

  const toast = getToast();

  // Limit visible toasts to 5
  const visibleToasts = $derived(toast.items.slice(0, 5));

  const icons: Record<string, string> = {
    success: 'i-lucide-check-circle',
    error: 'i-lucide-x-circle',
    warning: 'i-lucide-alert-triangle',
    info: 'i-lucide-info',
  };
</script>

<div
  class="toast-region"
  role="region"
  aria-live="polite"
  aria-label="Notifications"
>
  {#each visibleToasts as t (t.id)}
    <div
      class="toast toast-{t.type}"
      role="status"
      aria-atomic="true"
      transition:fly={{ x: 300, duration: 250 }}
    >
      <span class={icons[t.type]} aria-hidden="true" />
      <div class="toast-message">{t.message}</div>
      {#if t.action}
        <Button size="sm" variant="outline" onclick={() => { t.action?.onclick(); toast.remove(t.id); }}>
          {t.action.label}
        </Button>
      {/if}
      <button
        class="toast-close"
        onclick={() => toast.remove(t.id)}
        aria-label="Dismiss"
      >
        <span class="i-lucide-x" />
      </button>
    </div>
  {/each}
</div>

Shell Integration

Create the context once in the root layout and render the container. ToastContainer is re-exported through shell/index.ts.

<!-- src/routes/[[locale=locale]]/+layout.svelte -->
<script lang="ts">
  import { setToastContext } from '$lib/state/toast.svelte';
  import { ToastContainer } from '$lib/components/shell';

  let { children } = $props();

  setToastContext();
</script>

<div class="app-shell">
  <Sidebar />
  <main>
    {@render children()}
  </main>
  <ToastContainer />
</div>

Usage Examples

Read the context with getToast(), then call success/error/warning/info with a message string and optional duration.

Form Submission

<script lang="ts">
  import { getToast } from '$lib/state/toast.svelte';
  import { superForm } from 'sveltekit-superforms';

  const toast = getToast();

  const { enhance } = superForm(data.form, {
    onResult({ result }) {
      if (result.type === 'success') {
        toast.success('Settings saved');
      } else if (result.type === 'failure') {
        toast.error('Failed to save settings');
      }
    },
  });
</script>

Error with Manual Dismiss

<script lang="ts">
  import { getToast } from '$lib/state/toast.svelte';

  const toast = getToast();

  async function fetchData() {
    try {
      const res = await fetch('/api/data');
      if (!res.ok) throw new Error('Failed to fetch');
      return await res.json();
    } catch (error) {
      // duration 0 = stays until manually dismissed
      toast.error('Failed to load data. Check your connection and try again.', 0);
    }
  }
</script>

Clipboard Copy

<script lang="ts">
  import { getToast } from '$lib/state/toast.svelte';

  const toast = getToast();

  async function copyToClipboard(text: string) {
    await navigator.clipboard.writeText(text);
    toast.success('Copied to clipboard');
  }
</script>

Persistent Toast with Action

<script lang="ts">
  import { getToast } from '$lib/state/toast.svelte';

  const toast = getToast();

  function notifyUpdateAvailable() {
    toast.show({
      type: 'info',
      message: 'A new version is available.',
      duration: 0, // stays until dismissed or the action fires
      action: { label: 'Reload', onclick: () => location.reload() },
    });
  }
</script>

Consumer: $lib/components/shell/UpdatePrompt.svelte shows exactly this toast — once per session, 30 minutes after updated.current (from $app/state) flips true and the user hasn't navigated.


Stacking Behavior

Scenario Behavior
Multiple toasts Stack vertically, newest at the bottom of the array
Max visible 5 (ToastContainer slices toast.items to the first 5)
Auto-dismiss Each toast removes itself after its duration (default 5000ms; 0 = manual only)
Page navigation Persist toasts (the context lives in the root layout)

Styling

/* UnoCSS utilities + custom properties */
.toast {
  --toast-bg: var(--color-surface);
  --toast-border: var(--color-border);
  --toast-icon: var(--color-text-muted);

  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 1rem;
  background: var(--toast-bg);
  border: 1px solid var(--toast-border);
  border-radius: 0.5rem;
  box-shadow: var(--shadow-lg);
  min-width: 300px;
  max-width: 400px;
}

.toast-success {
  --toast-icon: var(--color-success);
  --toast-border: var(--color-success-border);
}

.toast-error {
  --toast-icon: var(--color-error);
  --toast-border: var(--color-error-border);
}

.toast-warning {
  --toast-icon: var(--color-warning);
  --toast-border: var(--color-warning-border);
}

Accessibility

Requirement Implementation
Screen reader aria-live="polite" on container
Role role="alert" on each toast
Focus Don't steal focus, toasts are informational
Dismiss Button with aria-label="Dismiss"
Reduced motion Respect prefers-reduced-motion for animations
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: none;
  }
}

Mobile Behavior

Pattern Desktop Mobile
Position Top-right Top-center, full width with padding
Max width 400px 100% - 2rem
Dismiss Click X or action Swipe right or tap X
Stacking 5 visible 3 visible
@media (max-width: 640px) {
  .toast-container {
    left: 1rem;
    right: 1rem;
    max-width: none;
  }

  .toast {
    width: 100%;
    max-width: none;
  }
}

Component Location

src/lib/
├── state/
│   └── toast.svelte.ts                       # Toast context factory
└── components/
    └── composites/
        └── toast/
            ├── ToastContainer.svelte         # Toast renderer (re-exported via shell/index.ts)
            ├── Toaster.svelte                # Alternate display variant
            └── index.ts

← Back to Blueprint

Geht dieses Pattern noch besser? Sag uns, wie.

Feedback geben