Examples
Real-world patterns, each one live. Copy the snippet next to it — they only use the public API.
Undo a destructive action
Apply the change immediately and offer a way back, instead of asking for confirmation first.
const remove = $((item: string) => {
items.value = items.value.filter((i) => i !== item);
toast("Conversation archived", {
description: item,
action: {
label: "Undo",
onClick$: $(() => (items.value = [...items.value, item])),
},
});
});Submitting a form
toast.promise narrates the request; unwrap() lets the handler continue with the result. Try an address without @.
<form
preventdefault:submit
onSubmit$={async (_, form) => {
const email = new FormData(form).get("email") as string;
const result = toast.promise(subscribe(email), {
loading: "Subscribing…",
success: $((data: { email: string }) => `${data.email} is on the list`),
error: $((err: Error) => err.message),
});
// unwrap() resolves/rejects like the original promise
try {
await result?.unwrap();
form.reset();
} catch {
// the error toast is already showing
}
}}
>Reporting progress
Update a loading toast by id as work advances, then settle it.
const id = toast.loading("Uploading report.pdf", { description: "0%" });
for (let p = 0; p <= 100; p += 20) {
await new Promise((r) => setTimeout(r, 400));
toast.loading("Uploading report.pdf", { id, description: `${p}%` });
}
toast.success("report.pdf uploaded", { id, description: "2.4 MB" });Server action results
React to a routeAction$ result in a task, guarded with isServer: the toast store is a module-level singleton, so a toast created while rendering on the server would never reach the browser.
import { component$, isServer, useTask$ } from "@qwik.dev/core";
import { Form, routeAction$ } from "@qwik.dev/router";
import { toast } from "qwik-sonner";
export const useSaveProfile = routeAction$(async (data) => {
// …persist
return { ok: true, name: data.name as string };
});
export default component$(() => {
const action = useSaveProfile();
useTask$(({ track }) => {
const result = track(() => action.value);
// The toast store is module-global: never create toasts during SSR.
if (!result || isServer) return;
if (result.ok) toast.success(`Saved ${result.name}`);
else toast.error("Could not save your profile");
});
return <Form action={action}>{/* fields */}</Form>;
});Fetch requests
A Response that isn't ok becomes an error toast automatically — no need to throw yourself.
toast.promise(fetch("/api/report"), {
loading: "Generating report…",
success: "Report ready",
// A Response with ok: false lands here with "HTTP error! status: 500"
error: $((message: string) => message),
});Above a modal dialog
<dialog>.showModal() lives in the browser's top layer, above any z-index. A Toaster with topLayer joins it there.
// A second Toaster, promoted to the top layer
<Toaster id="over-modals" position="top-center" topLayer />
// Inside the <dialog>
toast.success("Saved", { toasterId: "over-modals" });Several toasters
Route toasts to a named Toaster with toasterId — for example a top-center one for global announcements next to the default corner.
<Toaster />
<Toaster id="announcements" position="top-center" />
toast.info("Maintenance tonight", { toasterId: "announcements" });Live toast counter
import { component$ } from "@qwik.dev/core";
import { useSonner } from "qwik-sonner";
export const ToastCounter = component$(() => {
const { toasts } = useSonner();
return <span>{toasts.value.length} on screen</span>;
});Following your app's theme
Pass a Signal to theme and flip it with the rest of your UI — this site does exactly that with its theme switch.
export default component$(() => {
const theme = useSignal<"light" | "dark">("light");
return (
<>
<Toaster theme={theme} />
<button onClick$={() => (theme.value = theme.value === "dark" ? "light" : "dark")}>
Toggle theme
</button>
<Slot />
</>
);
});