Feedback

Toaster

Shows the toasts you send with toast("Saved") from @/lib/ouro/toast: render it once, at the root of your app, inside OuroProvider if you use one, or alone. Adding Toaster installs toast with it. Toasts stack at the bottom of the screen, full width on phones by default; className adjusts viewport layout/clearance, not each toast's appearance. Up to three show, and later ones wait their turn, a destructive toast ahead of the others. They pause while the pointer is over them or a toast has focus, F6 moves focus to them, newest first, and a swipe down or sideways dismisses one: a toast follows the finger, a quick flick is enough, and one let go short of that springs back. Each toast is read to screen readers once, its message and description: politely, and a destructive toast assertively, so it interrupts. Every toast and its buttons stay reachable by screen readers, a touch screen reader too, while the stack is collapsed, and focus in a toast fans the stack out and pauses it, however it got there, even while a pointer passes over and away.

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

In its source: app/components/navigation/mobile-nav.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/toaster#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/toaster

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/toaster#v1.2.13 ourostack/ouro-ui/toaster-tests#v1.2.13
import { Toaster } from "@/components/ouro/toast/toaster";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<OuroProvider>
  <App />
  <Toaster />
</OuroProvider>
// Anywhere in the app:
toast("Recipe saved", { tone: "success" });

Usage

Render one Toaster. A second one warns in development and shows nothing until the first unmounts, then takes over.

The viewport is portaled to the body. A variable set only on an app-shell wrapper does not reach it: put app clearance variables on a real ancestor of the portal (such as :root) or declare them through the viewport's supplied classes. Do not construct arbitrary-value class names from runtime numbers. A width override changes the width, not the anchoring mode or centering. Bottom overrides replace the default phone safe-area expression; the app owns its clearance and safe area. Shared --popover, --popover-foreground and --radius theme tokens style the toast surface and also affect other components; a wrapper cannot style this portal.

Use instead when

  • Use Alert instead for a message that stays on the page, and Dialog when the user must answer before going on.

Props

Type: ToasterProps.

className
Type: string
Layout classes on the actual portaled toast viewport, not on each toast. For example, bottom-[calc(5rem+max(1rem,env(safe-area-inset-bottom)))] clears persistent bottom controls. A bottom override replaces the default safe-area expression; include the appropriate inset in your own clearance. Use statically written classes matching your app's visibility/breakpoints, which need not match the Toaster's sm breakpoint.
closeLabel
Type: string
Default: "Dismiss"
The accessible name of each toast's dismiss button, which shows an icon only.
label
Type: string
Default: "Notifications"
The name screen readers give the region the toasts appear in. Translate it with the rest of your interface.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • duration: Not a prop. Each toast takes its own duration: toast("Saved", { duration: 8000 }).
  • position: Not a prop. Toasts default to the bottom: full width on phones, at the bottom end from sm; className adjusts viewport layout.
  • richColors: Not a prop. Each toast takes its own tone: toast("Saved", { tone: "success" }).
  • theme: Not a prop. Dark mode follows the .dark class, as in shadcn.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Toast appearance comes from a toast's tone and shared theme tokens, not viewport classes. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: Toaster owns toast-content alignment. Use <Toaster /> without the alignment class; a parent wrapper cannot align its portaled content.

toast()

Shows a toast: a short message that appears at the bottom of the screen, is read to screen readers once, and dismisses itself. Call it from anywhere, such as an event handler or an action's result, after rendering <Toaster /> once. Returns the toast's id; toast.dismiss(id) closes it, and toast.dismiss() closes them all.

import { toast } from "@/lib/ouro/toast";

Example

toast("Recipe saved", { tone: "success" });
toast("Recipe deleted", { action: { label: "Undo", onClick: restore } });
toast("Couldn't save", { tone: "destructive", description: "Check your connection." });

Usage

A toast called before the Toaster mounts waits for it. Up to three show at once, the newest in front; later ones wait their turn, a destructive toast ahead of the others, and each one's countdown starts when it shows. Hovering or focusing them (or pressing F6) pauses the countdown, and a swipe down or sideways dismisses one. A toast with an action stays until it is dismissed unless you pass a duration.

Use instead when

  • Use Alert instead when the message must stay on the page, such as a form's errors, and Dialog when the user must answer before going on.

Parameters

message
Type: string
options
Type: ToastOptions
{}

Returns

string

Types

ToastAction

A button in a toast: { label: "Undo", onClick: undo }. Pressing it runs onClick, then dismisses the toast.

interface ToastAction {
  /** The button's text: a short verb, such as "Undo" or "View". */
  label: string;
  /** Runs when the button is pressed, before the toast is dismissed. */
  onClick: () => void;
}

ToastOptions

Options for toast.

interface ToastOptions {
  /**
   * Color role from the theme, named after its token, shown as an icon: `success` for
   * a check, `warning` and `destructive` for a problem, `primary` for information.
   * `secondary`, the default, has no icon. Sonner's `error` is `destructive` and its
   * `info` is `primary`.
   * @default "secondary"
   */
  tone?:
    | "primary"
    | "secondary"
    | "destructive"
    | "success"
    | "warning"
    | (string & { 'error is "destructive"; info is "primary"; danger is "destructive"': never })
    | undefined;
  /** A second line under the message. */
  description?: ReactNode;
  /** A button in the toast, such as Undo: `{ label: "Undo", onClick: undo }`. Pressing it dismisses the toast. */
  action?: ToastAction | undefined;
  /**
   * How long the toast stays once it shows, in milliseconds. Hovering or focusing the
   * toasts pauses the countdown. `0` or `Infinity` keeps it until it is dismissed.
   *
   * A toast with an `action` stays until it is dismissed unless you pass a duration: a
   * timed Undo can run out before a keyboard or screen reader user reaches it (WCAG 2.2.1).
   * @default 5000, or until dismissed with an action
   */
  duration?: number | undefined;
  /** An id of your own. A toast with the id of one still showing replaces it in place. */
  id?: string | undefined;

  /** Not an option. Ouro UI uses `tone`: `toast("Saved", { tone: "success" })`. */
  type?: { 'Use toast("Saved", { tone: "success" })': never };
  /** Not an option. Ouro UI uses `tone`: `toast("Saved", { tone: "success" })`. */
  variant?: { 'Use toast("Saved", { tone: "success" })': never };
  /** Not an option. Ouro UI uses `duration`, in milliseconds: `toast("Saved", { duration: 8000 })`. */
  timeout?: { 'Use toast("Saved", { duration: 8000 })': never };
  /** Not an option. The message is the first argument: `toast("Saved", { description: "…" })`. */
  title?: { 'The message is the first argument: toast("Saved")': never };
}