Toast

Transient status messages anchored to a corner. Toasts stack with a compressed fan when several are queued, hover to expand, follow a promise from loading to its result, and update in place.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add toast

Add <Toaster /> once to your root layout: it renders the fixed viewport and is required for toasts to appear.

app/layout.tsxTSX
import { Toaster } from "@/components/ui/toast"export default function RootLayout({ children }) {  return (    <html>      <body>        {children}        <Toaster />      </body>    </html>  )}

Usage#

usage.tsxTSX
import { toast, useToast } from "@/components/ui/toast"// Imperative: works anywhere (Server Action results, event handlers, outside React)toast("File saved.")toast.success("Uploaded.")toast.error("Something went wrong.")toast.warning("Disk space low.")toast.info("Version 2.4 available.")// Work in flight: one toast that follows the promise to its resulttoast.promise(saveChanges(), {  loading: "Saving…",  success: "Saved.",  error: "Couldn't save.",})// Beside the element that raised it, instead of the cornertoast.success("Link copied", { anchor: event.currentTarget })// Close one toast by id, or all of themtoast.dismiss(id)toast.dismiss()// Hook: same API, inside React componentsfunction MyComponent() {  const { toast } = useToast()  return <Button onClick={() => toast("Done!")}>Save</Button>}

Variants#

Five semantic variants: default, success, warning, destructive, and info. Every toast is the same neutral elevated card (border + shadow); the status colour is carried by the icon, so the variants stay calm and never get lost on the corner. The buttons fire the real thing.

Changes saved successfully.

File uploaded.

profile-photo.png is ready.

Storage almost full.

You have 200 MB remaining.

Upload failed.

The server returned a 413 error.

New version available.

Refresh the page to get v2.4.0.

Loading#

toast.loading shows a spinner and stays up until you raise the same id again with the result. The card is updated in place: the spinner cross-fades into the result icon and the title rolls to its new line, so it reads as one piece of work finishing rather than a second message arriving.

Uploading 3 files…

Keep this tab open.

Upload complete.

3 files are ready.

Promise#

toast.promise does the loading-then-result dance for you. Each message can be a title, a full options object, or a function of the resolved value (or the error). It hands the promise back, so await toast.promise(save(), …) still returns the value or throws.

Saving changes…

Saved 12 changes.

Couldn't save.

You're offline. We'll retry when you reconnect.

Update in place#

Give a toast an id and raising it again never stacks a copy. The card moves to the front, its timer restarts, and it replays a short cue so the repeat still registers: a pop for a confirmation, a shake for an error or a warning. Press either button several times to see it.

Anchored#

Pass an anchor and the toast becomes a small bubble beside the element that raised it: a “Copied” next to the copy button rather than a card in the far corner. It sits above the element (below it when the element is near the top of the screen), stays up for 1.5 seconds, and closes as soon as the page scrolls or resizes, since it would otherwise drift away from what it describes. The bubble itself is hidden from assistive tech; an always-mounted live region announces it instead.

Link copied

With action#

Pass an action object with a label and onClick handler. The button appears below the description and is announced as an accessible action by screen readers.

Stacking#

When multiple toasts are queued they compress into a fan: older toasts peek behind the newest one at decreasing scale and offset. Hover (or focus) the stack to expand all toasts to their full size with a 16px gap. Up to three toasts show in the collapsed fan; any beyond that are hidden until older ones are dismissed. On a touch screen, swipe a toast to the right to dismiss it: the card follows your finger.

Persistent#

Pass duration: Infinity to keep the toast until the user explicitly dismisses it. Useful for critical alerts. For work in progress, prefer toast.loading, which persists on its own and resolves in place.

FAQ#

Both share the same API. The imperative `toast(...)` works anywhere, including Server Action results, event handlers, and code outside React, while `useToast()` returns the same `toast` plus a `dismiss` function for use inside components. Reach for the hook only when you want the matching `dismiss` helper at hand.