block
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.
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>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>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>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>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>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>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>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.
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>The prop reference below documents three surfaces:
<Toaster> you mount once.toast(...) (and the typed shortcuts below).useToastHistory() composable (Svelte), or getToastHistoryHandles() from @pindoba/astro-toast/runtime (Astro).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."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.
4000Default auto-dismiss duration in milliseconds. Use `Infinity` on a single toast to make it sticky.
falseTrack dismissed toasts in the history log. Required for the notification center.
trueWhen `stacked` is true, fan the deck out into a vertical list on pointer/focus enter.
"right"Notification center layout. `"right" | "left" | "top" | "bottom"` opens it as a side drawer; `false` opens it as a centered modal dialog.
5Maximum number of concurrently visible toasts. Older toasts are evicted when the limit is exceeded.
50Maximum entries kept in the history log.
Fired whenever the history array changes. Receives the full list.
truePause auto-dismiss timers (and the progress bar) while the toast is hovered or focus is inside it.
"bottom-right"Default viewport corner for spawned toasts. Per-toast overrides are honored.
trueRender a thin progress bar at the bottom of each toast that animates to zero across the duration.
trueRender concurrent toasts as a deck (overlapping cards). Disable for a flat vertical list.
If provided AND `enableHistory` is true, the history is persisted to localStorage under this key.
trueEnable horizontal swipe-to-dismiss via pointer events (40px threshold).
Render an action button inline with the dismiss control. `onClick` fires when pressed.
Render inside the element with this id instead of the global viewport — useful for scoped toasters inside dialogs.
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.
When true, render as a custom (non-Alert) toast row. Internal, set by `toast.custom()`.
Override the toaster's default duration for this toast (ms). Use `Infinity` for a sticky toast.
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.
Custom icon — an `HTMLElement` or an icon name string. Defaults to the icon for the toast `type`.
Stable identifier. If omitted, an id is hashed from title + content. Reusing an id triggers dedupe (the existing toast updates in place).
Fired only when the toast is removed by the auto-dismiss timer.
Fired when the toast is removed from the screen (manual dismiss OR auto-close).
Override the toaster's default viewport corner for this toast.
trueWhen `toast.update()` patches an existing toast, whether to restart the timer from zero. Use `false` for purely visual updates that shouldn't extend lifetime.
falsePlay a short audio cue. `true` plays a built-in WebAudio blip per type; a string is treated as an audio URL.
Heading text. When omitted the toast renders in a compact row layout (icon + content + close).
"neutral"Visual feedback. `"loading"` defaults to a sticky duration.
trueInitial unread state when pushed to the history log.
Empty the history log.
Read-only array of history entries, newest first.
Whether the built-in notification dialog is currently open.
Mark every history entry as read.
Mark a single history entry as read by id.
Open or close the built-in notification dialog. Pass `true`/`false` to force a state.
Number of entries with `unread: true`.