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.
Every toast row is an Alert card: the tone stamp and the title head it, the message is its content, the dismiss button is the alert’s own, and an action sits in its footer. A toast without a title is the compact single-row alert. The alert is the row’s live region — live="polite" (role="status"), or live="assertive" (role="alert") for danger toasts — so the row wrapper itself stays silent and nothing is announced twice. toast.custom() rows render no Alert and keep the live region on the wrapper.
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.
---
// GENERATED from packages/demos/toast/default.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}
data-ir-root="toast/default"
>
<Button data-ir="1">Primary</Button>
<Button data-ir="2">Success</Button>
<Button data-ir="3">Danger</Button>
<Button data-ir="4">Warning</Button>
<Button data-ir="5">Neutral</Button>
</div>
<Toaster position="bottom-right" data-ir-root="toast/default" />
<script>
import { hydrate } from "./_client/default.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/default"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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.
---
// GENERATED from packages/demos/toast/emphasis.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}
data-ir-root="toast/emphasis"
>
<Button data-ir="1">Default (secondary)</Button>
<Button data-ir="2">Primary (loud)</Button>
<Button data-ir="3">Tertiary (quietest)</Button>
</div>
<Toaster position="bottom-right" data-ir-root="toast/emphasis" />
<script>
import { hydrate } from "./_client/emphasis.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/emphasis"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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 tone color, icon, and content all animate cleanly without unmount. Only the resolved state lands in history — loading toasts are transient and never recorded.
---
// GENERATED from packages/demos/toast/promise.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row" })}
data-ir-root="toast/promise"
>
<Button data-ir="1">Resolve in 1.5s</Button>
<Button data-ir="2">Reject in 1.5s</Button>
</div>
<Toaster position="bottom-right" data-ir-root="toast/promise" />
<script>
import { hydrate } from "./_client/promise.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/promise"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</script>Add an action to render a button in the toast card’s footer (the dismiss control stays in its header). Use it for quick recoveries like undo, retry, or open-in-new-tab.
---
// GENERATED from packages/demos/toast/actions.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row" })}
data-ir-root="toast/actions"
>
<Button data-ir="1">With action button</Button>
<Button data-ir="2">Title-less</Button>
</div>
<Toaster position="bottom-right" data-ir-root="toast/actions" />
<script>
import { hydrate } from "./_client/actions.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/actions"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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.
---
// GENERATED from packages/demos/toast/dedupe.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "sm", direction: "row" })} data-ir-root="toast/dedupe">
<Button data-ir="1">Fire same id</Button>
</div>
<Toaster position="bottom-right" data-ir-root="toast/dedupe" />
<script>
import { hydrate } from "./_client/dedupe.ts";
const roots = [...document.querySelectorAll('[data-ir-root="toast/dedupe"]')];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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.
---
// GENERATED from packages/demos/toast/positions.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { grid } from "@pindoba/styled-system/patterns";
const positions = [
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
] as const;
---
<div class={grid({ columns: 3, gap: "sm" })} data-ir-root="toast/positions">
<!--ir:1-->
{positions.map((position) => (
<Button data-ir="2" data-ir-key={position}>
<Fragment set:html="<!--ir:3-->" />
{position}
<Fragment set:html="<!--/ir-->" />
</Button>
))}
<!--/ir-->
</div>
<Toaster position="bottom-right" data-ir-root="toast/positions" />
<script>
import { hydrate } from "./_client/positions.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/positions"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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.
---
// GENERATED from packages/demos/toast/stacked.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row" })}
data-ir-root="toast/stacked"
>
<Button data-ir="1">Spawn random toast</Button>
</div>
<Toaster
position="bottom-right"
stacked
expandOnHover
data-ir-root="toast/stacked"
/>
<script>
import { hydrate } from "./_client/stacked.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/stacked"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</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.
---
// GENERATED from packages/demos/toast/history.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Toaster from "@pindoba/astro-toast";
import { stack } from "@pindoba/styled-system/patterns";
---
<div
class={stack({ gap: "sm", direction: "row" })}
data-ir-root="toast/history"
>
<Button data-ir="1">Push toast</Button>
<Button data-ir="2">
Open notifications (<Fragment set:html="<!--ir:3-->" />
{0}
<Fragment set:html="<!--/ir-->" />)
</Button>
<Button data-ir="4">Mark all read</Button>
<Button data-ir="5">Clear history</Button>
</div>
<Toaster
position="bottom-right"
enableHistory
storageKey="pindoba-toast-demo"
data-ir-root="toast/history"
/>
<script>
import { hydrate } from "./_client/history.ts";
const roots = [
...document.querySelectorAll('[data-ir-root="toast/history"]'),
];
for (let i = 0; i < roots.length; i += 2) hydrate(roots.slice(i, i + 2));
</script>Each center entry is an Alert card too — the same card as the toast it recorded, without the live region and the dismiss button. The latest timestamp sits in the card header’s trailing slot (the end of the row for a title-less entry).
The center’s tray is a Panel root (data-component="toast-center"), so its
surface is a set of Panel props rather than hand-painted CSS. Its defaults —
a recessed surface.ground fill, an lg corner, no border and no shadow —
reproduce the tray exactly as it looked before. Because the tray sits inside the
center’s Dialog panel, radius="inner" resolves the concentric corner against
that dialog:
import { connectToaster } from "@pindoba/core-toast";
const api = connectToaster({ center: { radius: "inner", border: "muted" } });
// api.centerBodyProps is the tray's Panel bag
The <Toaster> blocks render a single, app-wide center, so the surface is set
through the core option rather than a component prop — there is no per-instance
demo for it.
The Props tab 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.