Toast
Short notifications raised from anywhere with toast.success(), toast.error() and toast.promise(). Stacked, pausable, swipeable and announced to screen readers. Built on Base UI.
Usage
Mount one Toaster near the root of your app, then raise toasts from anywhere, including outside React.
import { Toaster, toast } from "@azmr/ui/toast";
// Once, in the root layout:
<Toaster />
// Anywhere:
toast.success("Invoice sent");Toaster is a client component. A toast raised before it has mounted has nowhere to render, so put it in the layout rather than in a page.
This site has a Toaster in its layout, so the demos below raise real toasts in the corner of the screen.
Types
"use client";
import { Button } from "@azmr/ui/button";
import { toast } from "@azmr/ui/toast";
export default function ToastTypes() {
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => toast("Draft saved")}>
Plain
</Button>
<Button
variant="outline"
onClick={() =>
toast.success("Invoice sent", { description: "Aroha will get it within a minute." })
}
>
Success
</Button>
<Button variant="outline" onClick={() => toast.info("New version available")}>
Info
</Button>
<Button variant="outline" onClick={() => toast.warning("Your plan renews tomorrow")}>
Warning
</Button>
{/* Errors are announced urgently (role="alert"); the others politely. */}
<Button
variant="outline"
onClick={() => toast.error("Could not save", { description: "Check your connection." })}
>
Error
</Button>
</div>
);
}| Call | Icon | Announced |
|---|---|---|
toast(title) | none | politely |
toast.success | check | politely |
toast.info | info | politely |
toast.warning | triangle | politely |
toast.error | alert | urgently (role="alert") |
toast.loading | spinner | politely |
Each takes (title, options). Options: description, duration, id, priority, action and onClose. Every call returns the toast's id.
Icons are decorative; the type is also in the text you write. Do not rely on colour alone.
Actions and updating a toast
- Quarterly report
- Team photo
"use client";
import { Button } from "@azmr/ui/button";
import { toast } from "@azmr/ui/toast";
import { useState } from "react";
export default function ToastActions() {
const [items, setItems] = useState(["Quarterly report", "Team photo"]);
function remove(name: string) {
setItems((all) => all.filter((n) => n !== name));
toast(`Deleted “${name}”`, {
// The same id replaces the toast instead of stacking another.
id: `deleted-${name}`,
action: {
label: "Undo",
onClick: () => setItems((all) => (all.includes(name) ? all : [...all, name])),
},
});
}
return (
<div className="flex flex-col items-start gap-3">
<ul className="flex flex-col gap-2 text-sm">
{items.map((name) => (
<li key={name} className="flex items-center gap-3">
{name}
<Button size="sm" variant="ghost" onClick={() => remove(name)}>
Delete
</Button>
</li>
))}
{items.length === 0 && <li className="text-muted-foreground">Nothing left.</li>}
</ul>
</div>
);
}action: { label, onClick } adds a button, and the toast dismisses itself after it runs. Undo is the classic use.
Giving a toast an id that is already showing updates it instead of stacking another, so repeating an action does not flood the screen. toast.update(id, { title, type, … }) changes a toast in place, and toast.dismiss(id) removes one (toast.dismiss() removes them all).
Promises
"use client";
import { Button } from "@azmr/ui/button";
import { toast } from "@azmr/ui/toast";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export default function ToastPromise() {
return (
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() =>
toast.promise(
wait(1500).then(() => 12),
{
loading: "Exporting…",
success: (rows) => `Exported ${rows} rows`,
error: "Export failed",
},
)
}
>
Export (succeeds)
</Button>
<Button
variant="outline"
onClick={() =>
toast
.promise(
wait(1500).then(() => Promise.reject(new Error("Disk full"))),
{
loading: "Exporting…",
success: "Exported",
error: (e: Error) => `Export failed: ${e.message}`,
},
)
.catch(() => {})
}
>
Export (fails)
</Button>
</div>
);
}toast.promise(promise, { loading, success, error }) shows a loading toast, then replaces it with the result. success and error can be functions of the value or the error. It returns the same promise, so it still rejects for you to handle; a failure is announced urgently.
Timing
A toast dismisses itself after duration (5000 ms by default; set it on the Toaster or per toast). duration: 0 keeps it until dismissed, and toast.loading never times out. The timer pauses while the pointer is over the stack or focus is inside it, so nobody loses a toast they are reading.
Do not use a toast for anything the person must act on. Auto-dismissing messages are easy to miss, so keep errors that need a decision in the page (or an AlertDialog).
Stacking and position
The Toaster shows limit toasts (3 by default); older ones wait behind and the stack fans out on hover or focus. position is top-left, top-center, top-right, bottom-left, bottom-center or bottom-right (default), and new toasts come in from that edge. The stack sits on the --z-toast layer, above dialogs and menus.
Motion
Toasts slide in from their edge, fade out, and the stack reflows on --motion-duration-slow. Under prefers-reduced-motion those collapse to near zero. While a finger is dragging a toast there is no easing, so it tracks exactly.
Accessibility
| Key | Action |
|---|---|
| F6 | Moves focus into the notifications region. Tab then moves between the actions and close buttons. |
| TaborShift+Tab | Moves between the toasts' buttons. The timers pause while focus is inside. |
| EnterorSpace | Activates the focused action or close button. |
- The region is named "Notifications" (change it with
label) and is a polite live region. Errors are also written to a separate alert, so they are spoken at once. - The close button is labelled "Dismiss notification"; its icon is decorative.
- A toast can be swiped away toward the edge it came from on touch.
Props
Prop
Type
Prop
Type