Free pack

Toast — <mv-toaster>

Sonner-style stacked notifications: a compact stack that expands on hover, paused timers, swipe to dismiss, promises and in-place updates by id — through an imperative toast() API.

CategoryOverlays
TypeWeb Component (<mv-toaster>)
Statusstable
Keywordstoast, sonner, notification, snackbar, swipe, promise, live-region

When to use

Avoid when

Install

node scripts/add.mjs toast --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["toast"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/toast/toast.js, components/toast/toast.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="toast-demo" style="display:grid;gap:1rem;justify-items:center">
  <div style="display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;max-width:34rem">
    <button class="mv-button" data-variant="outline" data-toast="default">Default</button>
    <button class="mv-button" data-variant="outline" data-toast="success">Success</button>
    <button class="mv-button" data-variant="outline" data-toast="error">Error</button>
    <button class="mv-button" data-variant="outline" data-toast="warning">Warning</button>
    <button class="mv-button" data-variant="outline" data-toast="info">Info</button>
    <button class="mv-button" data-variant="outline" data-toast="promise">Promise</button>
    <button class="mv-button" data-variant="outline" data-toast="update">Update in place</button>
  </div>
  <p style="margin:0;font-size:.8125rem;color:var(--mv-fg-muted);text-align:center">Hover the stack to expand it · swipe a toast to dismiss it · <strong style="color:var(--mv-fg);font-weight:500">Alt + T</strong> to reach it from the keyboard</p>
  <mv-toaster position="bottom-right" close-button></mv-toaster>
</div>
<script type="module">
  // In an app: import { toast } from "./components/toast/toast.js";
  const { toast } = await customElements.whenDefined("mv-toaster");
  const wait = (ms) => new Promise((r) => setTimeout(r, ms));
  const demos = {
    default: () => toast("Event created", {
      description: "Sunday, Dec 6, 2026 at 9:00 AM",
      action: { label: "Undo", onClick: () => toast("Event creation undone") },
    }),
    success: () => toast.success("Changes saved", { description: "Your public profile has been updated." }),
    error: () => toast.error("Couldn't send", {
      description: "The server isn't responding. Check your connection.",
      action: { label: "Retry", onClick: () => demos.promise() },
    }),
    warning: () => toast.warning("Storage almost full", { description: "480 MB left of 15 GB." }),
    info: () => toast.info("New version available", { description: "It will be installed on your next restart." }),
    promise: () => toast.promise(wait(1800), {
      loading: "Importing 24 contacts…",
      success: { message: "24 contacts imported", description: "They now appear in your address book." },
      error: "Import failed",
    }),
    update: async () => {
      const id = "sync-demo";
      for (let i = 1; i <= 3; i++) {
        toast.loading(`Syncing… ${i}/3`, { id, description: "Folder “Client projects”" });
        await wait(900);
      }
      toast.success("Sync complete", { id, description: "3 files updated." });
    },
  };
  document.getElementById("toast-demo").addEventListener("click", (e) => {
    const kind = e.target.closest("[data-toast]")?.dataset.toast;
    if (kind) demos[kind]();
  });
</script>

API

Attributes

NameTypeDefaultDescription
positiontop-left | top-center | top-right | bottom-left | bottom-center | bottom-rightbottom-rightAnchor corner; the stack and swipe direction adapt.
maxnumber3Number of visible toasts (the rest wait, hidden).
durationnumber4000Default duration (ms).
gapnumber14Gap between cards when expanded (px).
expandbooleanAlways expanded.
rich-colorsbooleanTints cards by type.
close-buttonbooleanClose button on hover / focus (always visible on touch).
labelstringNotificationsAccessible name of the region.

Properties

NameTypeDescription
toastsobject[]Read-only: displayed toasts.

Methods

NameDescription
toast(message, options) → idExported from toast.js (also MvToaster.toast). options: description, type (success | error | warning | info | loading), action { label, onClick(e, toast) }, cancel { label, onClick }, duration (ms or Infinity), id (same id = in-place update with a small bounce), icon (Node or null), closeButton, dismissible, important, toaster (selector), onDismiss, onAutoClose. Creates an <mv-toaster> when needed.
toast.success / .error / .warning / .info / .loading(message, options)Typed shortcuts.
toast.promise(promise, { loading, success, error }, options)Loading, then success/error in the same card; success/error accept a string, a function of the result or an object { message, description, … }. Returns the promise (with .id).
toast.dismiss(id?)Dismisses one toast, or all of them.
element.add(options) / element.dismiss(id?)Same operations on a specific toaster.

Events

NameDescription
mv-toastNew toast. detail: { id, type }.
mv-dismissdetail: { id, reason } — timeout, swipe, close, escape, action, cancel, dismiss.

CSS classes

NameDescription
mv-toast / -icon / -content / -title / -description / -action / -cancel / -closeGenerated, stylable parts; data-type on .mv-toast.

CSS variables

NameDefaultDescription
--mv-toast-width22.25remCard width (full width below 600 px).
--mv-toaster-offset1.5remDistance from the screen edges.
--mv-toast-peek12pxHow much each card peeks out in the compact stack.
--mv-toast-bgCard background.
--mv-toast-radiusCard radius.

Accessibility

Section region (implicit role) named “Notifications (Alt+T)”, aria-live=polite (additions and text updates are announced); important:true switches the card to role=alert. Alt+T moves focus to the latest toast; each card is focusable, Escape dismisses it and focus moves to the next one, then back to the original element. Timers pause on hover, on focus and while the tab is hidden. Animations are reduced automatically (prefers-reduced-motion).