Toast
Short notices that stack at an edge of the screen, pause on hover and close on their own or with a swipe.
Types
A title, an optional description, and an icon per type. Hover the stack to fan it out; swipe a toast to dismiss it.
Code
toast("Event has been created");
toast("Event has been created", { description: "Monday, January 3rd at 6:00pm" });
toast.success("Event has been created");
toast.info("Be at the area 10 minutes before the event time");
toast.warning("Event start time cannot be earlier than 8am");
toast.error("Event has not been created");
toast.loading("Loading data");Usage
import { Toaster, Toast } from "@elements/ui/toast";
import { toast } from "@elements/ui/toast";
<!-- once, in the app shell -->
<Toaster richColors />
<!-- anywhere in browser code -->
<Button onclick={() => toast.success("Event created", {
description: "Sunday, December 3 at 9:00 AM",
action: { label: "Undo", onClick: () => undo() },
})}>Create event</Button>
<Button onclick={() => toast.promise(save(form), {
loading: "Saving...",
success: (doc) => `Saved ${doc.name}`,
error: "Could not save",
})}>Save</Button>
<!-- declarative: open is two-way -->
<Toast open={state.saved} title="Saved" type="success" />
<!-- anchored: next to the button instead of in the stack -->
<Button onclick={(e) => toast("Copied", {
anchor: e.currentTarget as Element,
positioner: { side: "top", sideOffset: 8, arrow: true },
})}>Copy</Button>Examples
Action and cancel
The action closes the toast after it runs, unless its onClick calls preventDefault(). Cancel runs and closes.
Code
toast("Event has been created", {
action: { label: "Undo", onClick: () => undo(), altText: "Undo from the events page" },
});
toast("Delete draft?", {
cancel: { label: "Keep", onClick: () => keep() },
action: { label: "Delete", onClick: () => remove() },
});Promise
A loading toast that turns into the result or the error when the promise settles. The icon box and the width never change.
Code
toast.promise(save(), {
loading: "Saving...",
success: (doc) => `${doc.name} has been saved`,
error: "Could not save",
});
// a function, an object result, and unwrap()
let { unwrap } = toast.promise(() => upload(file), {
loading: "Uploading...",
success: (r) => ({ message: "Uploaded", description: r.url }),
error: (err) => `Upload failed: ${err.message}`,
});
await unwrap();Position
Six positions. Swipe directions follow the position: bottom-right swipes down or right.
Code
<Toaster position="top-center" />
// or per toast
toast("Moved", { position: "top-left" });Options
Toaster attributes: keep the stack expanded, status colors, a close button on each toast, inverse colors, and a light or dark theme whatever the page's mode.
Code
<Toaster expand richColors closeButton invert duration={4000} visibleToasts={3} />
// dark toasts on a light page, light on a dark one (or "system")
<Toaster theme="dark" />Lifetime and dismissal
A toast that stays, one that cannot be dismissed by the user, callbacks, and dismissing everything.
Code
let id = toast.loading("Uploading...");
toast.success("Uploaded", { id }); // same id: updates in place
toast("Stays until closed", { duration: Infinity });
toast("Cannot be swiped away", { dismissible: false, duration: Infinity });
toast("Connection lost", { important: true }); // assertive announcement
toast("Logged", { onDismiss: (t) => log(t), onAutoClose: (t) => log(t) });
toast.dismiss(id);
toast.dismiss(); // every toastDeclarative
<Toast open> adds a toast while open is true and writes false back when it closes itself.
Code
<Button onclick={() => state.saved = true}>Save</Button>
<Toast open={state.saved} type="success" title="Saved" action={{ label: "View" }}
description="Your changes are live." onOpenChange={(open) => log(open)} />Uncontrolled
A literal open makes a Toast uncontrolled: it shows when it renders and closes itself, so a list of them is a list of toasts.
Code
<Button onclick={() => state.batch.push({ id: state.batch.length + 1 })}>Add a toast</Button>
<!-- A literal open: each one shows when it renders and closes itself. -->
<Toast e:for={b of state.batch} open title={`Upload ${b.id} finished`} closeButton />Anchored
A toast with an anchor sits next to that element instead of in the stack, with an optional arrow. Pressed again, the same id updates it in place.
Code
<Button onclick={(e) => onCopy(e)}>Copy</Button>
function onCopy(e: MouseEvent): void {
navigator.clipboard.writeText(code);
toast("Copied", {
id: "copied", // pressed again: the same toast
anchor: e.currentTarget as Element,
positioner: { side: "top", sideOffset: 8, arrow: true },
duration: 1500,
});
}Updates and data
Showing a toast again with the same id updates it in place and replays data-updated, which this demo uses for a pulse. data carries your own values to a function description.
Code
interface Inbox { unread: number }
// The same id updates the toast in place; data-updated replays each time,
// so CSS can pulse it. A function description is given the toast.
toast("New messages", {
id: "inbox",
data: { unread: state.unread },
description: (t) => `${(t.data as Inbox).unread} unread`,
class: "pulse-on-update",
});
toast.update("inbox", (t) => ({ title: "All caught up", type: "success", data: { unread: 0 } }));Several toasters
A Toaster with an id takes only the toasts sent to it with toasterId.
Code
<Toaster />
<Toaster id="side" position="top-left" offset={{ top: 72, left: 24 }} gap={8} expand
swipeDirections={["left"]} icons={{ info: "bell" }}
toastOptions={{ class: "side-toast" }} />
toast("Goes to the side toaster", { toasterId: "side" });Custom content
toast.custom() places your own node in a toast, inside the chrome or headless. A function title re-renders when what it reads changes.
Code
// Your own node, inside the toast's chrome
toast.custom(() => card("A custom toast with default styling"), { unstyled: false });
// Headless: no chrome; the node owns its look and its buttons
toast.custom((id) => {
let box = document.createElement("div");
box.className = "my-toast";
// ... title, text, and a button that calls toast.dismiss(id)
return box;
});
// A function title or description re-renders when what it reads changes
toast(() => `${state.files} files uploaded`, { description: () => `${state.files * 2} MB` });Styling
Inline style and a class on every part of one toast, and styles on its buttons. A Toaster takes style, offset and toastOptions for all of its toasts.
Code
toast("Styled", {
style: "border-style: dashed",
classes: { title: "my-title", description: "my-description", actionButton: "my-action" },
action: { label: "Open", style: "font-style: italic" },
cancel: { label: "Later", style: "text-decoration: underline" },
});
<Toaster style="--my-mark: 1" offset={16} mobileOffset={{ bottom: "16px" }}
toastOptions={{ style: "...", classes: { closeButton: "my-close" } }} />Over a dialog
The region is in the top layer and is raised when a toast arrives, so a toast shown from a modal dialog is drawn above it and announced. The hotkey, Tab, hover, clicks and swipes reach it while the dialog stays open.
Code
<Dialog title="Settings">
<Button slot="trigger">Open dialog</Button>
<Button onclick={() => toast("Saved from a dialog")}>Save</Button>
</Dialog>