Toast
A short note that something just happened — a booking created, a change saved, a save that failed. Toasts slide in at the top and never take focus. A plain note leaves on its own; one with an action or an error stays until it's dismissed. Okami's Toaster over the vendored sonner one, with sonner's toast beside it.
Playground
import { toast } from "@/components/shared/toast";
toast.success("Booking created", {
description: "Skin fade · Friday 10:00 with Martina",
});Success & error
The two outcomes of nearly every action, each marked with a thin Hugeicons line, never filled: an ink check for success, a cross in the palette's one red for an error. A description, set small, adds the specifics — which booking, what to do next.
import { toast } from "@/components/shared/toast";
toast.success("Booking created", {
description: "Skin fade · Friday 10:00 with Martina",
});
toast.error("Couldn't create the booking", {
description: "That slot was just taken. Pick another.",
});Loading
For work that takes a moment, toast.promise shows a spinner, then turns into the outcome in place — one toast, not three. To drive it by hand, keep the id from toast.loading and pass it to the next call.
import { toast } from "@/components/shared/toast";
toast.promise(createBooking(input), {
loading: "Creating booking",
success: "Booking created",
error: "Couldn't create the booking",
});
// By hand:
const id = toast.loading("Sending reminders");
toast.success("Reminders sent", { id });With an action
An action, the black primary button, offers the one obvious next step. Undo is the best of them: it lets a reversible action skip its confirmation dialog. A toast with an action stays until the action is taken or the toast is closed with its X: nobody should lose an Undo to a timer.
import { toast } from "@/components/shared/toast";
toast("Booking cancelled", {
description: "Jana Nováková · Skin fade, Friday 10:00",
action: { label: "Undo", onClick: () => restoreBooking(id) },
});Mounting
Mount the Toaster once, in the root layout, inside the intl provider. Feature code then only imports toast — from this module, never from sonner directly.
// app/[locale]/layout.tsx
import { Toaster } from "@/components/shared/toast";
<NextIntlClientProvider>
{children}
<Toaster />
</NextIntlClientProvider>API reference
toast
| Prop | Type | Default | Description |
|---|---|---|---|
| toast(title, options?) | string | number | — | A plain message, no mark. Returns the toast's id; so do the calls below. |
| toast.success / .error | (title, options?) | — | The outcomes: an ink check, or a red cross. An error stays until it's dismissed, so it can be read. |
| toast.warning / .info | (title, options?) | — | A heads-up (an ink alert) or a neutral note (a quiet info mark). |
| toast.loading | (title, options?) | — | A spinner that stays until the same id is updated or dismissed. |
| toast.promise | (promise, { loading, success, error }) | — | Loading, then the outcome, in one toast. |
| toast.dismiss | (id?) | — | Closes one toast, or all of them. |
Options
| Prop | Type | Default | Description |
|---|---|---|---|
| description | ReactNode | — | A quiet second line: which booking, what next. |
| action | { label, onClick } | — | One next step, as the black primary button. Pressing it closes the toast. |
| cancel | { label, onClick } | — | A ghost pill before the action. Rarely needed. |
| id | string | number | — | Updates the toast with this id in place instead of adding one. |
| duration | number | 4000 | Milliseconds on screen, paused while hovered or the tab is hidden. With an action, or for an error, it's Infinity (and the X shows) unless the call sets one. |
Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-center" | "bottom-right" | … | "top-center" | Where toasts appear. |
| expand | boolean | true | Toasts list one under another rather than stacking into a pile. |
| visibleToasts | number | 3 | How many show at once; the rest wait their turn. |
| closeButton | boolean | false | A small ghost X on every toast. |
| id | string | — | Scopes a second Toaster; only toasts with the same toasterId land on it. |
Theme is fixed to light and every part is drawn by Okami; the rest of sonner's Toaster props pass through.
Guidelines
- A toast reports; it never asks. Anything that needs a decision is a Dialog, and anything people must not miss stays on the page.
- Say what happened, to what, in the past tense: “Booking created”, “Changes saved”. An error says what failed and, in the description, what to do next.
- One toast per event. Update a loading toast in place with
toast.promiseor its id instead of adding another. - For a reversible action, skip the confirmation dialog and offer Undo in the toast.
- Anything the reader has to act on or read stays: a toast with an action, or an error, waits until it's dismissed (WCAG 2.2.1). The toast module does this; don't shorten it with a
duration. - Don't toast what the page already shows — a saved field that visibly changed needs no second announcement.