Skip to content
design
Components

Toast

A short message at the edge of the screen about what the crew just did, which leaves by itself or when dismissed.

On this page
Messages about what the crew just did: a saved record, a deleted entry with Undo, a saved route with three actions, and a failed sync.
Code
import { Button } from "@bosunkit/components/button";
import { ToastProvider, useToast, type ToastApi } from "@bosunkit/components/toast";
import { Download, Share2, SquareArrowOutUpRight, Undo2 } from "lucide-react";
import type { ReactElement } from "react";

export const description: string =
  "Short messages about what the crew just did. A saved record leaves by itself, a deleted entry offers Undo and " +
  "counts down the seconds it can still be pressed, a saved route offers three actions from one split button, an " +
  "export shows a spinner until it finishes, and a failed sync stays until it is dismissed.";

function Buttons(): ReactElement {
  const toast: ToastApi = useToast();
  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-md)" }}>
      <Button
        onClick={(): void => {
          toast.show({
            kind: "success",
            title: "Record saved",
            description: "Impeller service is marked complete.",
          });
        }}
      >
        Save record
      </Button>
      <Button
        variant="ghost"
        onClick={(): void => {
          toast.show({
            title: "Log entry deleted",
            countdown: true,
            actions: [
              {
                label: "Undo",
                icon: Undo2,
                onAction: (): void => {
                  toast.show({ kind: "success", title: "Entry restored" });
                },
              },
            ],
          });
        }}
      >
        Delete entry
      </Button>
      <Button
        variant="ghost"
        onClick={(): void => {
          toast.show({
            kind: "success",
            title: "Route saved",
            href: "#props",
            description: "Falmouth to the Isles of Scilly.",
            actions: [
              { label: "Open", icon: SquareArrowOutUpRight, onAction: (): void => undefined },
              { label: "Share with crew", icon: Share2, onAction: (): void => undefined },
              { label: "Export as GPX", icon: Download, onAction: (): void => undefined },
            ],
          });
        }}
      >
        Save route
      </Button>
      <Button
        variant="ghost"
        onClick={(): void => {
          let finish: ReturnType<typeof setTimeout> | undefined;
          let cancel: (error: Error) => void = (): void => undefined;
          const exported: Promise<string> = new Promise<string>(
            (resolve: (file: string) => void, reject: (error: Error) => void): void => {
              cancel = reject;
              finish = setTimeout((): void => {
                resolve("falmouth-scilly.gpx");
              }, 3000);
            },
          );
          toast
            .track(exported, {
              running: {
                title: "Exporting the route as GPX",
                actions: [
                  {
                    label: "Cancel",
                    onAction: (): void => {
                      clearTimeout(finish);
                      cancel(new Error("cancelled"));
                    },
                  },
                ],
              },
              done: (file: string) => ({ kind: "success", title: "Route exported", description: file }),
              failed: { kind: "error", title: "Could not export the route" },
            })
            .catch((): void => undefined);
        }}
      >
        Export route
      </Button>
      <Button
        variant="ghost"
        onClick={(): void => {
          toast.show({
            kind: "error",
            key: "sync-failed",
            title: "Could not sync the log",
            description: "The shore link is down. The entries stay on this device until it is back, and Bosun tries again every minute.",
            footer: "Shore link, down since 13:12 UTC",
          });
        }}
      >
        Fail a sync (repeat it)
      </Button>
      <Button
        variant="ghost"
        onClick={(): void => {
          toast.show({
            kind: "attention",
            persistent: true,
            title: "Check the bilge",
            description: "Stays until you dismiss it.",
          });
        }}
      >
        Keep until dismissed
      </Button>
    </div>
  );
}

export default function Showcase(): ReactElement {
  return (
    <ToastProvider viewportLabel="Notifications" dismissLabel="Dismiss" moreActionsLabel="More actions">
      <Buttons />
    </ToastProvider>
  );
}

A toast confirms an action in a few words: "Record saved", "Log entry deleted", with one more line and the actions the crew can take about it. ToastProvider owns the stack and the region it is announced in; useToast() returns show and dismiss. The newest toast stands nearest the edge the stack is anchored to, at the bottom for a stack at the bottom, and rises there while it fades in as the older ones glide away from the edge to make room. Every toast shows; a stack taller than the screen scrolls. A toast leaves by itself, by a swipe to the right, or by its dismiss button. As it leaves, a toast at the end of the screen slides out past the screen's edge while it fades, one in the middle shrinks a little as it fades, and the toasts that stay glide into its place in the same motion. Under reduced motion a toast only fades and the others take their places at once. It is built on Base UI's toast, so its timer pauses while the pointer or focus is on it and starts again when they leave, and F6 moves focus into the region. A toast takes the height its text needs, so a short confirmation and a long explanation stack without overlapping.

actions lists what the crew can do about the message, most likely first, each with a label and an optional icon drawn before it (Undo takes Lucide's Undo2). The first is the toast's button. Any others open from the menu of a split button around it, named by the provider's moreActionsLabel, so the toast keeps one row of controls however many actions it has; the menu opens above the toast stack, and the toast holds its timer while the menu is open. Every action is a touch target as tall as the dismiss button, and the toast closes after any of them runs.

track(task, { running, done, failed }) shows a toast while a task runs, with a spinner as its mark, which turns in place into done or failed when the task settles; the result's timer starts then. An action on the running toast that ends the task, such as Cancel, closes it and its result is not shown. A running toast the crew dismissed comes back with the result, since a finished or failed task is news.

href makes the title a link to what the message is about, such as the saved route or the restored entry; a path inside the application goes through the provider's link component, and following it closes the toast.

footer adds one line under a rule at the toast's foot, in a card footer's small dim type: where the message comes from and since when ("Shore link, down since 13:12 UTC"), with a time's reference written out. It lines up with the title and the description.

countdown shows how long a toast has left: the seconds after its action's label and a bar along its foot that shortens each second. It is for an action that can be pressed only for a while, such as Undo after a deletion, and it stops whenever the toast's timer pauses. A toast with a countdown leaves when its time runs out, whatever its kind. onClose runs once when a toast leaves without one of its actions run, by its timer, its dismiss button or a swipe, so a deletion that offers Undo is made final there, at the moment Undo leaves the screen.

persistent keeps a toast until the crew press its dismiss button, whatever its kind and timeout. A toast that waits for the crew this way, like an error, an attention message or a running task, keeps its place however many toasts arrive after it. With a limit on the provider, the toasts that leave by themselves take the places left, newest first, and the older ones wait out of sight with their time held until a place opens. A message that can repeat takes a key ("sync-failed"): showing it again while it is on the screen updates that toast in place, restarts its timer and counts the repeat, so a copy of it is never stacked. The count is a badge after the title ("×3") that grows and fades in once on each repeat, and the toast stacks one edge under itself for its second showing and two from its third, so a message that keeps coming reads as a small pile.

The kinds are info, success, error, attention and discovery, each with its own mark so a kind is never told by color alone. There is no warning kind: that word is an alert's priority, and a toast is not an alert. An error and an attention message stay until the crew dismiss them; the others leave after a few seconds. The caller translates every word, so the component adds no catalog keys.

01Confirm, never alertA toast has no priority, never flashes and uses no alert color. A lost connection or a failed save the crew must act on is a banner or an inline message.
02Nothing that matters leaves unreadAn error or an attention message stays until dismissed, and the timer of any toast pauses while the crew look at it.
03One button, then it closesA toast shows its most likely action, such as Undo, as one button, puts any others in that button's menu, and closes after one runs.

Props

PropTypeDefaultDescription
childrenrequiredReactNode–The page; anything inside it can call useToast.
viewportLabelrequiredstring–The name of the region the toasts are in for a screen reader ("Notifications"); translated by the caller.
dismissLabelrequiredstring–The name of each toast's dismiss button ("Dismiss"); translated by the caller.
moreActionsLabelrequiredstring–The name of the button that opens a toast's further actions ("More actions"); translated by the caller.
placementToastPlacement | undefined"bottom-end"Where the toasts stack; the bottom end of the screen by default.
limitnumber | undefinedNumber.POSITIVE_INFINITYHow many toasts show at once; every one by default, the stack scrolling when it is taller than the screen. With a limit, a toast that waits for the crew (one that stays until dismissed, an error or attention message, a running task) still always shows; the toasts that leave by themselves take the places left, newest first and at least one, and the older ones wait out of sight with their time held.
defaultTimeoutnumber | undefined6000How long a toast of a kind that leaves on its own stays by default, in milliseconds. 6,000 by default.
motionboolean | undefinedtrueWhether a toast slides and fades in and out; on by default. Under reduced motion it only fades, whatever this says.
Do

Log entry deleted

Confirm what the crew just did, in a few words, with the one action that undoes it.
Don't

Anchor alarm is off. (disappears in 6 s)

Don't tell the crew something they must act on in a toast: it leaves, and a screen reader may never reach it.
  • Task dialog: the dialog for a decision the crew must make before going on.
  • Popover: a panel that stays open until it is closed.
  • Alert card: the alert, which has a priority, flashes until acknowledged and stays.