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", wrap: "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", wrap: "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>

    <Toaster /> Props

    props · 14 total
    prop type default req description
    position "top-left""top-right""top-center""bottom-left""bottom-right""bottom-center" "bottom-right" Default viewport corner for spawned toasts. Per-toast overrides are honored.
    stacked boolean false Render concurrent toasts as a deck (overlapping cards). Disable for a flat vertical list.
    expandOnHover boolean true When stacked is true, fan the deck out into a vertical list on pointer/focus enter.
    duration number 4000 Default auto-dismiss duration in milliseconds. Use Infinity on a single toast to make it sticky.
    limit number 5 Maximum number of concurrently visible toasts. Older toasts are evicted when the limit is exceeded.
    pauseOnHover boolean true Pause auto-dismiss timers (and the progress bar) while the toast is hovered or focus is inside it.
    swipeToDismiss boolean true Enable horizontal swipe-to-dismiss via pointer events. Threshold is 40px.
    showProgressBar boolean true Render a thin progress bar at the bottom of each toast that animates to zero across the duration.
    enableHistory boolean false Track dismissed toasts in the history log. Required for the notification center.
    maxHistoryItems number 50 Maximum entries kept in the history log.
    historyDrawer "right""left""top""bottom"false "right" Notification center layout. Side-drawer (right/left/top/bottom) or false for a centered modal dialog.
    defaultEmphasis "primary""secondary""tertiary" "secondary" Default emphasis for pushed toasts when the input does not specify one. Per-toast overrides via `toast({ emphasis: ... })` are honored.
    storageKey string undefined If provided AND enableHistory is true, the history is persisted to localStorage under this key.
    onHistoryChange (history: HistoryEntry[]) => void undefined Callback fired whenever the history array changes. Receives the full list.

    toast() API

    toast(input) and its convenience methods all return the toast id (a string). Pass that id back to toast.dismiss(id) or use toast.promise() for async tracking.

    props · 13 total
    prop type default req description
    id string auto-generated Stable identifier. If omitted, an id is hashed from title + content. Reusing an id triggers dedupe.
    title string undefined Heading text. When omitted the toast renders in a compact row layout (icon + content + close).
    content stringHTMLElement((container: HTMLElement) => void) undefined Body text, an HTMLElement, or a render function `(container) => void`. The Svelte block accepts a string only inside Toaster's row template; use toast.custom for arbitrary content.
    type "success""danger""warning""neutral""loading" "neutral" Visual feedback. success, danger, warning, neutral, or loading. Loading defaults to sticky duration.
    duration number Toaster duration Override the toaster's default duration for this toast. Use Infinity for sticky.
    position ToastPosition Toaster position Override the toaster's default position for this toast.
    action { label: string; onClick: (e: Event) => void } undefined Render a button inline with the dismiss control. onClick fires when pressed.
    containerId string undefined Render this toast inside the element with this id instead of the global viewport (useful for scoped toasters inside dialogs).
    sound booleanstring false Play a short audio cue. true plays a built-in WebAudio blip per type; a string is treated as an audio URL.
    unread boolean true Initial unread state when pushed to the history log. Defaults to true.
    resetDuration boolean 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.
    onDismiss () => void undefined Callback fired when the toast is removed from the screen (manual dismiss OR auto-close).
    onAutoClose () => void undefined Callback fired only when the toast is removed by the auto-dismiss timer.

    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.

    useToastHistory() composable

    Available from @pindoba/svelte-toast; the Astro runtime exposes the same atoms via getToastHistoryHandles() from @pindoba/astro-toast/runtime.

    props · 7 total
    prop type default req description
    history HistoryEntry[] [] Read-only array of history entries, newest first.
    unreadCount number 0 Number of entries with `unread: true`.
    isCenterOpen boolean false Whether the built-in notification dialog is currently open.
    markAsRead (id: string) => void - Mark a single history entry as read by id.
    markAllAsRead () => void - Mark every history entry as read.
    clearHistory () => void - Empty the history log.
    toggleCenter (force?: boolean) => void - Open or close the built-in notification dialog. Pass true/false to force a state.