block

Toast

A block-level notification system. Mount a single <Toaster> near the root of the app and fire feedback from anywhere with the imperative toast() API. Toasts render at any of the six viewport corners, support promise tracking, swipe-to-dismiss, dedupe-by-id, and a built-in notification center.

Default

Mount a <Toaster> once and fire toasts from button handlers (or any other code path). Each call returns the toast’s id, which you can pass to toast.dismiss() or toast.update() later. Six built-in types — primary, success, danger, warning, neutral, and loading — drive the icon and color palette.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}>
  <Button data-toast-demo="primary">Primary</Button>
  <Button data-toast-demo="success">Success</Button>
  <Button data-toast-demo="danger">Danger</Button>
  <Button data-toast-demo="warning">Warning</Button>
  <Button data-toast-demo="neutral">Neutral</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";

  document.querySelectorAll<HTMLElement>("[data-toast-demo]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const type = btn.dataset.toastDemo;
      if (type === "primary") {
        toast.primary({
          title: "New mention",
          content: "Jane tagged you in a doc.",
        });
      } else if (type === "success") {
        toast.success({ title: "Saved", content: "Your changes are live." });
      } else if (type === "danger") {
        toast.danger({
          title: "Couldn't save",
          content: "Network is unreachable.",
        });
      } else if (type === "warning") {
        toast.warning({
          title: "Heads up",
          content: "Disk is almost full.",
        });
      } else if (type === "neutral") {
        toast.neutral({ title: "FYI", content: "Cache rebuilt." });
      }
    });
  });
</script>

Emphasis

Toasts default to secondary emphasis — a calm, tinted Alert variant that doesn’t fight the rest of the page. Pass emphasis: "primary" for can’t-miss messages (saturated filled card), or emphasis: "tertiary" for the quietest variant. Set defaultEmphasis on <Toaster> to change the global default.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}>
  <Button data-emphasis-demo="default">Default (secondary)</Button>
  <Button data-emphasis-demo="primary">Primary (loud)</Button>
  <Button data-emphasis-demo="tertiary">Tertiary (quietest)</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";

  document
    .querySelectorAll<HTMLElement>("[data-emphasis-demo]")
    .forEach((btn) => {
      btn.addEventListener("click", () => {
        const variant = btn.dataset.emphasisDemo;
        if (variant === "default") {
          toast.success({
            title: "Saved",
            content: "Default toasts use secondary emphasis.",
          });
        } else if (variant === "primary") {
          toast.danger({
            title: "Outage detected",
            content: "Reserve primary for can't-miss messages.",
            emphasis: "primary",
          });
        } else if (variant === "tertiary") {
          toast.neutral({
            title: "Heads up",
            content: "Tertiary is the quietest variant.",
            emphasis: "tertiary",
          });
        }
      });
    });
</script>

Promise tracking

toast.promise() shows a sticky loading toast, then transitions in-place to a success or error toast based on the promise’s resolution. The DOM node is swapped so the new feedback color, icon, and content all animate cleanly without unmount. Only the resolved state lands in history — loading toasts are transient and never recorded.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row" })}>
  <Button data-toast-demo="promise-resolve">Resolve in 1.5s</Button>
  <Button data-toast-demo="promise-reject">Reject in 1.5s</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";

  document.querySelectorAll<HTMLElement>("[data-toast-demo]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const action = btn.dataset.toastDemo;
      if (action === "promise-resolve") {
        toast.promise(
          new Promise<string>((res) => setTimeout(() => res("ok"), 1500)),
          {
            loading: "Uploading…",
            success: "Uploaded",
            error: "Upload failed",
          },
        );
      } else if (action === "promise-reject") {
        toast
          .promise(
            new Promise<never>((_, rej) =>
              setTimeout(() => rej(new Error("503")), 1500),
            ),
            {
              loading: "Uploading…",
              success: "Uploaded",
              error: "Upload failed — try again",
            },
          )
          .catch(() => {
            /* swallow — toast already showed */
          });
      }
    });
  });
</script>

Action button

Add an action to render a secondary button inline next to the dismiss control. Use it for quick recoveries like undo, retry, or open-in-new-tab.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row" })}>
  <Button data-toast-action="with-action">With action button</Button>
  <Button data-toast-action="no-title">Title-less</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";

  document
    .querySelectorAll<HTMLElement>("[data-toast-action]")
    .forEach((btn) => {
      btn.addEventListener("click", () => {
        const action = btn.dataset.toastAction;
        if (action === "with-action") {
          toast.neutral({
            title: "Email archived",
            content: "Tap undo to restore it.",
            action: {
              label: "Undo",
              onClick: () => toast.success({ title: "Restored" }),
            },
          });
        } else if (action === "no-title") {
          toast.warning({
            content:
              "Title-less toasts use the row layout, like a compact alert.",
          });
        }
      });
    });
</script>

Deduplication

Calling toast() with an existing id reuses the active row instead of stacking a new one. The count badge updates, the timer resets, and the toast pops to the front of the deck. Deduped occurrences are also recorded in the history with a single entry whose count and timestamps accumulate.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row" })}>
  <Button data-toast-dedupe>Fire same id</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";

  document
    .querySelector("[data-toast-dedupe]")
    ?.addEventListener("click", () => {
      toast.success({
        id: "dedupe-demo",
        title: "Saved",
        content:
          "Same id — count badge ticks up and the toast pops to the front.",
      });
    });
</script>

Position overrides

A toast can override the toaster’s default position to land in a different viewport corner — useful for forcing a critical error to top-center while everything else flows from bottom-right.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { grid } from "@pindoba/styled-system/patterns";
---

<div class={grid({ columns: 3, gap: "sm" })}>
  <Button data-toast-position="top-left">Top left</Button>
  <Button data-toast-position="top-center">Top center</Button>
  <Button data-toast-position="top-right">Top right</Button>
  <Button data-toast-position="bottom-left">Bottom left</Button>
  <Button data-toast-position="bottom-center">Bottom center</Button>
  <Button data-toast-position="bottom-right">Bottom right</Button>
</div>

<Toaster position="bottom-right" />

<script>
  import { toast } from "@pindoba/astro-toast/runtime";
  import type { ToastPosition } from "@pindoba/astro-toast/runtime";

  document
    .querySelectorAll<HTMLElement>("[data-toast-position]")
    .forEach((btn) => {
      btn.addEventListener("click", () => {
        const position = btn.dataset.toastPosition as ToastPosition;
        toast.neutral({
          title: position,
          content: "Per-toast position override.",
          position,
        });
      });
    });
</script>

Stacked deck

Set stacked to render multiple toasts as an overlapping deck. With expandOnHover, hovering or focusing the deck fans the rows out vertically so the user can read and interact with all of them.

---
import Button from "@pindoba/astro-button";
import Toaster from "../Toaster.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row" })}>
  <Button data-toast-spawn>Spawn random toast</Button>
</div>

<Toaster position="bottom-right" stacked={true} expandOnHover={true} />

<script>
  import { toast, getToastStore } from "@pindoba/astro-toast/runtime";

  document
    .querySelector("[data-toast-spawn]")
    ?.addEventListener("click", () => {
      // Reapply the demo's Toaster config — needed when several demos share
      // one document and the user wants to see this demo's settings now.
      getToastStore().setConfig({ stacked: true, expandOnHover: true });
      const types = ["success", "danger", "warning", "neutral"] as const;
      const type = types[Math.floor(Math.random() * types.length)];
      toast[type]({
        title: type,
        content: `Spawned at ${new Date().toLocaleTimeString()}`,
      });
    });
</script>

History and notification center

Set enableHistory to keep dismissed toasts in a log. Pair with storageKey to persist across reloads. The built-in dialog is rendered for you — call toggleCenter() from the useToastHistory() composable (Svelte) or import from runtime (Astro) to open it. The dialog filters by All / Unread.

Notifications

    No notifications.

    ---
    import Button from "@pindoba/astro-button";
    import Toaster from "../Toaster.astro";
    import { stack } from "@pindoba/styled-system/patterns";
    ---
    
    <div class={stack({ gap: "sm", direction: "row" })}>
      <Button data-toast-history="push">Push toast</Button>
      <Button data-toast-history="open">
        Open notifications (<span data-history-unread-count>0</span>)
      </Button>
      <Button data-toast-history="mark-read">Mark all read</Button>
      <Button data-toast-history="clear">Clear history</Button>
    </div>
    
    <Toaster
      position="bottom-right"
      enableHistory={true}
      storageKey="pindoba-toast-demo"
    />
    
    <script>
      import {
        toast,
        toggleCenter,
        markAllAsRead,
        clearHistory,
        $unreadCount,
      } from "@pindoba/astro-toast/runtime";
    
      $unreadCount.subscribe((count) => {
        document
          .querySelectorAll<HTMLElement>("[data-history-unread-count]")
          .forEach((el) => (el.textContent = String(count)));
      });
    
      type DemoToast = {
        type: "primary" | "success" | "danger" | "warning" | "neutral";
        title: string;
        content: string;
      };
      const samples: DemoToast[] = [
        { type: "success", title: "Saved", content: "Changes published." },
        { type: "danger", title: "Failed", content: "Unable to reach server." },
        { type: "warning", title: "Heads up", content: "Disk almost full." },
        { type: "neutral", title: "FYI", content: "Cache rebuilt." },
        {
          type: "primary",
          title: "New mention",
          content: "Jane tagged you in a doc.",
        },
        { type: "success", title: "Deployed", content: "Build finished in 12s." },
        { type: "warning", title: "Slow query", content: "Check the indexes." },
        { type: "danger", title: "Payment failed", content: "Card was declined." },
        { type: "neutral", title: "Reminder", content: "Standup in 5 minutes." },
        { type: "primary", title: "Sync complete", content: "Latest data is in." },
      ];
    
      document
        .querySelectorAll<HTMLElement>("[data-toast-history]")
        .forEach((btn) => {
          btn.addEventListener("click", () => {
            const action = btn.dataset.toastHistory;
            if (action === "push") {
              const sample = samples[Math.floor(Math.random() * samples.length)]!;
              toast[sample.type]({ title: sample.title, content: sample.content });
            } else if (action === "open") {
              toggleCenter(true);
            } else if (action === "mark-read") {
              markAllAsRead();
            } else if (action === "clear") {
              clearHistory();
            }
          });
        });
    </script>

    API

    The prop reference below documents three surfaces:

    • Toaster — configuration props for the <Toaster> you mount once.
    • Toast — the options object passed to toast(...) (and the typed shortcuts below).
    • ToastHistory — the object returned by the useToastHistory() composable (Svelte), or getToastHistoryHandles() from @pindoba/astro-toast/runtime (Astro).

    Convenience methods

    • toast.success(input) / toast.danger(input) / toast.warning(input) / toast.neutral(input) / toast.loading(input) — shortcuts that set type for you.
    • toast.custom(content, options?) — render arbitrary DOM content (no Alert wrapper).
    • toast.dismiss(id) — force-close a toast.
    • toast.promise(promise, { loading, success, error }, options?) — track an async value through loading → success | error.
    props · 37 shown · 37 total
    Toaster
    defaultEmphasis
    "primary""secondary""tertiary"
    default "secondary"

    Default emphasis for pushed toasts when the input does not specify one. `"secondary"` is a calmer Alert variant. Per-toast overrides via `toast({ emphasis: "primary" })` are honored.

    duration
    number
    default 4000

    Default auto-dismiss duration in milliseconds. Use `Infinity` on a single toast to make it sticky.

    enableHistory
    boolean
    default false

    Track dismissed toasts in the history log. Required for the notification center.

    expandOnHover
    boolean
    default true

    When `stacked` is true, fan the deck out into a vertical list on pointer/focus enter.

    historyDrawer
    false"right""left""top""bottom"
    default "right"

    Notification center layout. `"right" | "left" | "top" | "bottom"` opens it as a side drawer; `false` opens it as a centered modal dialog.

    limit
    number
    default 5

    Maximum number of concurrently visible toasts. Older toasts are evicted when the limit is exceeded.

    maxHistoryItems
    number
    default 50

    Maximum entries kept in the history log.

    onHistoryChange
    (history: HistoryEntry[]) => void

    Fired whenever the history array changes. Receives the full list.

    pauseOnHover
    boolean
    default true

    Pause auto-dismiss timers (and the progress bar) while the toast is hovered or focus is inside it.

    position
    "top-left""top-right""top-center""bottom-left""bottom-right""bottom-center"
    default "bottom-right"

    Default viewport corner for spawned toasts. Per-toast overrides are honored.

    showProgressBar
    boolean
    default true

    Render a thin progress bar at the bottom of each toast that animates to zero across the duration.

    stacked
    boolean
    default true

    Render concurrent toasts as a deck (overlapping cards). Disable for a flat vertical list.

    storageKey
    string

    If provided AND `enableHistory` is true, the history is persisted to localStorage under this key.

    swipeToDismiss
    boolean
    default true

    Enable horizontal swipe-to-dismiss via pointer events (40px threshold).

    Toast
    action
    ToastAction

    Render an action button inline with the dismiss control. `onClick` fires when pressed.

    containerId
    string

    Render inside the element with this id instead of the global viewport — useful for scoped toasters inside dialogs.

    content
    stringHTMLElementToastContentRenderer

    Body text, an `HTMLElement`, or a render function `(container) => void`. The Svelte block accepts a string only inside the Toaster row template; use `toast.custom` for arbitrary content.

    custom
    boolean

    When true, render as a custom (non-Alert) toast row. Internal, set by `toast.custom()`.

    duration
    number

    Override the toaster's default duration for this toast (ms). Use `Infinity` for a sticky toast.

    emphasis
    "primary""secondary""tertiary"

    Visual emphasis. `"secondary"` (default) is a subtler, less attention-grabbing Alert variant — recommended for everyday feedback. `"primary"` is the loud filled variant — reserve for critical / can't-miss messages.

    icon
    stringHTMLElement

    Custom icon — an `HTMLElement` or an icon name string. Defaults to the icon for the toast `type`.

    id
    string

    Stable identifier. If omitted, an id is hashed from title + content. Reusing an id triggers dedupe (the existing toast updates in place).

    onAutoClose
    () => void

    Fired only when the toast is removed by the auto-dismiss timer.

    onDismiss
    () => void

    Fired when the toast is removed from the screen (manual dismiss OR auto-close).

    position
    "top-left""top-right""top-center""bottom-left""bottom-right""bottom-center"

    Override the toaster's default viewport corner for this toast.

    resetDuration
    boolean
    default true

    When `toast.update()` patches an existing toast, whether to restart the timer from zero. Use `false` for purely visual updates that shouldn't extend lifetime.

    sound
    booleanstring
    default false

    Play a short audio cue. `true` plays a built-in WebAudio blip per type; a string is treated as an audio URL.

    title
    string

    Heading text. When omitted the toast renders in a compact row layout (icon + content + close).

    type
    "primary""success""danger""warning""neutral""loading"
    default "neutral"

    Visual feedback. `"loading"` defaults to a sticky duration.

    unread
    boolean
    default true

    Initial unread state when pushed to the history log.

    ToastHistory
    clearHistory required
    () => void

    Empty the history log.

    history required
    HistoryEntry[]

    Read-only array of history entries, newest first.

    isCenterOpen required
    boolean

    Whether the built-in notification dialog is currently open.

    markAllAsRead required
    () => void

    Mark every history entry as read.

    markAsRead required
    (id: string) => void

    Mark a single history entry as read by id.

    toggleCenter required
    (force?: boolean) => void

    Open or close the built-in notification dialog. Pass `true`/`false` to force a state.

    unreadCount required
    number

    Number of entries with `unread: true`.

    Type

    • Components
    • Blocks