Skip to content

Component · 20

Toast

Short messages that arrive in a corner and leave by themselves. Raised cards with a coloured rule, announced to screen readers, paused while you hover or focus them, and dismissible by swipe. Built on Base UI.

Set up once, use anywhere

Put one ToastProvider near the root. Then call useToast() from any component inside it (it must be a client component).

tsx
// app/layout.tsx: once, near the root
import { ToastProvider } from "@gnoud-ui/react";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <ToastProvider>{children}</ToastProvider>
      </body>
    </html>
  );
}

// anywhere below it
"use client";
import { Button, useToast } from "@gnoud-ui/react";

function SaveButton() {
  const toast = useToast();
  return (
    <Button onClick={() => toast.add({ title: "Saved", description: "Your changes are live." })}>
      Save
    </Button>
  );
}

Variants

Danger is announced to screen readers at once; the others wait until they are idle.

tsx
toast.add({ variant: "success", title: "Uploaded", description: "report.pdf is ready." });
toast.add({ variant: "danger", title: "Payment failed", description: "Your card was declined." });

With an action

One button, for the thing a person would want straight away. It keeps the toast on screen a little longer.

tsx
toast.add({
  title: "Message deleted",
  timeout: 8000,
  action: { label: "Undo", onClick: restore },
});

Stay until dismissed, or go quickly

timeout: 0 keeps it. Use that for things a person must not miss, and give them a way to close it.

tsx
toast.add({ title: "Connection lost", timeout: 0 });
toast.add({ title: "Quick one", timeout: 1500 });

Follow a promise

One toast that says what is happening, then what happened. It stays while the work is pending, and only starts counting down once it is done.

tsx
toast.promise(save(draft), {
  loading: "Saving draft…",
  success: (name) => ({ title: `Saved ${name}`, description: "Your draft is safe." }),
  error: (error) => ({ title: "Could not save", description: error.message }),
});

Props

PropTypeDefaultDescription
ToastProvider position"top-left" | "top-right" | "bottom-left" | "bottom-right""bottom-right"Where the stack sits.
ToastProvider limitnumber3Most toasts on screen at once. Older ones wait out of sight.
ToastProvider timeoutnumber5000Default milliseconds before a toast dismisses itself. 0 keeps it.
ToastProvider labelstring"Notifications"The region's name for screen readers.
useToast().add(options)ToastOptions → id-Show a toast. Returns its id.
useToast().close(id?)(id?) => void-Dismiss one toast, or all of them with no id.
options.title / descriptionReactNode-The message. Keep it short.
options.variant"info" | "success" | "warning" | "danger""info"The coloured rule. The words must carry the meaning.
options.timeoutnumberprovider'sMilliseconds before it goes. 0 keeps it until dismissed.
options.priority"low" | "high"danger: highHigh is announced to screen readers at once; low politely when they are idle.
options.action{ label, onClick }-One button on the toast, for example Undo.
options.idstringautoReusing an id updates that toast instead of adding another.
useToast().promise(promise, messages)Promise<T>-Follow a promise: loading, then success or error. Returns the promise, so you can still await or catch it.
messages.loading / success / errortext, options, or (value) => text | options-What each stage says. Each can carry its own variant and timeout. An error is shown as danger and announced at once.

Keyboard and screen readers: press F6 to move focus to the notifications region, then Tab through the toasts. A toast pauses its countdown while it is hovered or focused. Because a toast disappears, never put the only copy of important information in one: also show it where the person can find it again.