Toast

Transient status messages at the bottom of the screen. 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 stacktoast.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 fight the page. 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.

Minimal#

appearance: "minimal" trims the card to one compact row: a smaller icon, a medium-weight title and the action inline on the right. The close button rests hidden and appears on hover or focus; swipe or Esc still dismiss it. Set it on a single toast, or on <Toaster appearance="minimal" /> to make it the default for the whole app.

Changes saved.

Link copied to clipboard.

Couldn't reach the server.

Conversation archived.

Undo

Countdown#

countdown: true draws a hairline along the bottom edge that drains across the toast's duration, in the variant's colour. It pauses whenever the toast does (hover, focus, or the window losing focus), so it always shows the time that is really left. Most useful on an Undo toast, where the window to act matters.

File uploaded.

profile-photo.png is ready.

Message deleted.

Undo

Progress#

Pass progress (0 to 100) for work you can measure, and raise the same id as it advances: the bar slides to the new value without the pop a re-raise usually plays, and the title stays put. Raise the result without progress when the work is done.

Uploading 5 files…

2 of 5 done

Exporting video…

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 at the far bottom of the screen. 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.

API reference#

Toaster

Renders the fixed, bottom-centered Radix Toast viewport, the anchored bubbles and a polite live region. Mount it once in the root layout. Forwards the Radix Toast.Viewport props and native <ol> props to the viewport, and className merges last there (move it with bottom-6, say). The stack keeps ownership of the viewport height; your pointer and focus handlers run alongside its expand and collapse.

PropTypeDefaultDescription
appearance"card" | "minimal""card"Default look for every toast; a toast's own appearance wins.
countdownbooleanfalseDefault for every toast: draw the draining bar along the bottom edge.
swipeDirection"right" | "left" | "up" | "down""right"Direction a toast is swiped to dismiss it.
swipeThresholdnumber50Distance in pixels a swipe must travel to dismiss.
hotkeystring[]["F8"]Keys that move focus to the toast stack.
labelstring"Notifications ({hotkey})"Accessible name of the viewport region; {hotkey} is replaced with the keys.
Data attributeDescription
data-stateOn each toast: "open" or "closed" (Radix).
data-swipeOn each toast while swiped: "start", "move", "cancel" or "end" (Radix).
data-sideOn an anchored bubble: "top" or "bottom" of its element.
CSS variableDescription
--radix-toast-swipe-move-xHorizontal offset while a toast is being swiped; the card follows it.
--radix-toast-swipe-end-xHorizontal offset where a swipe ended, for the dismiss exit.
--toast-durationSet on the countdown bar: how long it takes to drain, the toast's duration.

toast()

Raises a toast from anywhere: toast("Saved."), toast("Saved.", options) or toast(options). Returns the toast's id.

PropTypeDefaultDescription
idstring-A stable id. Raising it again while on screen updates that toast in place and replays a pop or shake.
titleReact.ReactNode-The headline.
descriptionReact.ReactNode-Supporting text under the title.
variant"default" | "success" | "warning" | "destructive" | "info" | "loading""default"The status icon. destructive is announced assertively, loading shows a spinner and persists.
durationnumber5000Auto-dismiss delay in ms: 1500 when anchored, Infinity for loading. Pass Infinity to persist.
action{ label: string; onClick: () => void }-A button below the description, a Radix Action with the label as its altText.
appearance"card" | "minimal"Toaster'sminimal is one compact row with the action inline and the close button revealed on hover or focus.
countdownbooleanToaster'sA hairline along the bottom edge that drains across duration and pauses with the toast. Ignored when the toast persists.
progressnumber-0 to 100: shows a progress bar under the text. Raise the same id to advance it; updates skip the pop and keep the title still.
anchorHTMLElement | null-Shows a small icon-and-title bubble beside this element instead of the stack (no close button, no action). Closes on scroll or resize.

toast.success / warning / error / info / loading

Shortcuts: (title, options?) with the variant set (error sets destructive). Options are the toast() options without title and variant. Each returns the id.

toast.promise

toast.promise(promise, messages, options?): one toast that shows loading, then turns into the result in place. Returns the promise, so awaiting it still yields the value or throws.

PropTypeDefaultDescription
promise*Promise<T> | (() => Promise<T>)-The work to follow.
messages.loading*React.ReactNode-The title while pending.
messages.success*ReactNode | ToastOptions | ((value: T) => ReactNode | ToastOptions)-Shown on resolve, as a success toast.
messages.error*ReactNode | ToastOptions | ((error: unknown) => ReactNode | ToastOptions)-Shown on reject, as a destructive toast.

toast.dismiss

toast.dismiss(id?): closes one toast by id, or every open toast when called with no id.

useToast

Returns { toast, dismiss }, the same imperative API, for use inside components.

useToastStore

Low-level: subscribes to the queue and returns the current ToastData[], for a custom renderer.

TOAST_DURATION / ANCHORED_TOAST_DURATION

The default auto-dismiss delays in ms: 5000 for a stacked toast, 1500 for an anchored one.

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.