Documentation.

One API, both React and Vue. Copy, paste, own it.

Installation

The registry is served as JSON. Add it with the shadcn CLI — the files land in components/ui/sapa-toast/.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toaster.json

Mount the Toaster

Render <Toaster /> once, near the root of your app. After that you can call toast() from anywhere.

app/layout.tsx
import { Toaster } from "@/components/ui/sapa-toast/toaster"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster position="bottom-right" />
      </body>
    </html>
  )
}

Basic usage

Call toast() with a message. Everything else is optional.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-default.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

export default function ToastDefault() {
  return (
    <button
      type="button"
      onClick={() => toast("Event has been created")}
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      Show toast
    </button>
  );
}

Loading & dismiss

toast.loading() persists until you dismiss or replace it. Every call returns an id you can pass to toast.dismiss().

import { toast } from "@/components/ui/sapa-toast/toaster"

const id = toast.loading("Saving…") // persists until you change it
toast.dismiss(id)                    // dismiss one
toast.dismiss()                      // dismiss every toast

Types & rich colors

Success, error, warning and info — each with a semantic color + icon.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-types.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

const btn =
  "inline-flex h-9 items-center rounded-md border bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-muted";

export default function ToastTypes() {
  return (
    <div className="flex flex-wrap gap-2">
      <button
        type="button"
        className={btn}
        onClick={() => toast.success("Changes saved")}
      >
        Success
      </button>
      <button
        type="button"
        className={btn}
        onClick={() => toast.error("Something went wrong")}
      >
        Error
      </button>
      <button
        type="button"
        className={btn}
        onClick={() => toast.warning("Your session is about to expire")}
      >
        Warning
      </button>
      <button
        type="button"
        className={btn}
        onClick={() => toast.info("A new update is available")}
      >
        Info
      </button>
    </div>
  );
}

Title + description

A headline with a secondary line of detail.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-description.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

export default function ToastDescription() {
  return (
    <button
      type="button"
      onClick={() =>
        toast.success("Event created", {
          description: "Monday, January 3rd at 6:00pm",
        })
      }
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      With description
    </button>
  );
}

Action + cancel

Inline buttons, e.g. undo a destructive change.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-action.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

export default function ToastAction() {
  return (
    <button
      type="button"
      onClick={() =>
        toast("File deleted", {
          description: "report-2026.pdf was moved to trash.",
          duration: 8000,
          action: {
            label: "Undo",
            onClick: () => toast.success("Restored"),
          },
          cancel: { label: "Dismiss" },
        })
      }
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      With action
    </button>
  );
}

Promise

Drive one toast through loading → success/error.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-promise.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

function fakeUpload(): Promise<{ name: string }> {
  return new Promise((resolve, reject) =>
    setTimeout(() => {
      // Randomly resolve/reject to show both branches.
      if (Math.round(Date.now() / 1000) % 2 === 0) {
        resolve({ name: "avatar.png" });
      } else {
        reject(new Error("Network error"));
      }
    }, 1800),
  );
}

export default function ToastPromise() {
  return (
    <button
      type="button"
      onClick={() =>
        toast.promise(fakeUpload(), {
          loading: "Uploading file…",
          success: (data) => `${data.name} uploaded`,
          error: (err) => `Upload failed: ${(err as Error).message}`,
        })
      }
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      Promise
    </button>
  );
}

Progress

A determinate progress bar — update it in place, then finish as success.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-progress.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

function startUpload() {
  let value = 0;
  const id = toast.progress("Uploading avatar.png…", {
    value,
    description: "0%",
  });

  const timer = setInterval(() => {
    value = Math.min(100, value + Math.round(8 + Math.random() * 14));
    toast.progress("Uploading avatar.png…", {
      id,
      value,
      description: `${value}%`,
    });

    if (value >= 100) {
      clearInterval(timer);
      // Transition the same toast in place once the work is done —
      // clearing `progress` removes the bar from the success toast.
      toast.success("avatar.png uploaded", {
        id,
        description: undefined,
        progress: undefined,
      });
    }
  }, 350);
}

export default function ToastProgress() {
  return (
    <button
      type="button"
      onClick={startUpload}
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      Upload file
    </button>
  );
}

Custom content

Render any markup — JSX in React, a component in Vue.

$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-custom.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

export default function ToastCustom() {
  return (
    <button
      type="button"
      onClick={() =>
        toast.custom(
          <div className="flex items-center gap-3">
            <img
              src="https://avatars.githubusercontent.com/u/124599?v=4"
              alt=""
              className="size-10 rounded-full"
            />
            <div className="space-y-0.5">
              <p className="text-sm font-medium">Olivia Martin</p>
              <p className="text-muted-foreground text-xs">
                sent you a friend request
              </p>
            </div>
            <button
              type="button"
              className="bg-foreground text-background ml-2 inline-flex h-7 items-center rounded-md px-2.5 text-xs font-medium"
              onClick={() => toast.success("Accepted")}
            >
              Accept
            </button>
          </div>,
          { duration: 6000 },
        )
      }
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      Custom content
    </button>
  );
}

Positions

Six anchor positions, set per-toast or globally.

top-lefttop-centertop-rightbottom-leftbottom-centerbottom-right
$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-positions.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";
import type { ToastPosition } from "@/components/ui/sapa-toast/toast-store";

const POSITIONS: ToastPosition[] = [
  "top-left",
  "top-center",
  "top-right",
  "bottom-left",
  "bottom-center",
  "bottom-right",
];

export default function ToastPositions() {
  return (
    <div className="grid grid-cols-3 gap-2">
      {POSITIONS.map((position) => (
        <button
          key={position}
          type="button"
          onClick={() => toast(position, { position, type: "info" })}
          className="bg-background hover:bg-muted inline-flex h-9 items-center justify-center rounded-md border px-3 text-xs font-medium shadow-sm transition-colors"
        >
          {position}
        </button>
      ))}
    </div>
  );
}

Stack

Toasts collapse into a pile; hover to expand.

Try it:
$npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toast-stack.json
"use client";

import { toast } from "@/components/ui/sapa-toast/toaster";

const MESSAGES = [
  "Event has been created",
  "Your report is ready",
  "New comment on your post",
  "Payment received",
  "Deployment finished",
] as const;

export default function ToastStack() {
  return (
    <button
      type="button"
      onClick={() => {
        MESSAGES.forEach((m, i) =>
          setTimeout(() => toast(m, { description: "Just now" }), i * 220),
        );
      }}
      className="bg-background hover:bg-muted inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium shadow-sm transition-colors"
    >
      Stack toasts
    </button>
  );
}

Options

The second argument of every toast() call. Identical in React and Vue.

PropTypeDescription
titleReactNodeMain message — the first argument of toast().
descriptionReactNodeSecondary line of detail below the title.
durationnumberAuto-dismiss delay in ms (default 4000). Use Infinity to persist.
positionToastPositionOne of the six anchors — overrides the Toaster default.
iconReactNodeCustom leading icon; falls back to the type's default.
progressnumber0–100 — a determinate bar along the bottom edge (see toast.progress).
action{ label, onClick }Primary inline button, e.g. Undo.
cancel{ label, onClick }Secondary / dismiss button.
idstring | numberStable id — reuse it to update a toast in place.
onDismiss(toast) => voidCalled once the toast is removed, whatever closed it.
onAutoClose(toast) => voidCalled only when the duration ran out — not on close, swipe or action.

<Toaster> props

Defaults for the mounted toaster.

PropTypeDescription
positionToastPositionDefault position for toasts that don't set their own (default "bottom-right").
expandbooleanShow toasts as a flat list instead of a collapsible stack (default false).

Custom colors

The rich colors come from four semantic CSS variables — --sapa-success, --sapa-error, --sapa-warning and --sapa-info. The toast's typepicks one, and it does two jobs: it tints the icon tile — a soft wash, a hairline of the same hue, and the glyph — and it paints the bar along the card's bottom edge, whether that bar is counting down the auto-dismiss or tracking a progress value. The theme registry item installs them into your project on setup; to re-brand the toast, just override them in your own globals.css — no component changes needed.

TokenWhat it colors
--sapa-successSuccess tile and bottom-edge bar.
--sapa-errorError tile and bottom-edge bar.
--sapa-warningWarning tile and bottom-edge bar.
--sapa-infoInfo tile and bottom-edge bar.

Redefine any of them under :root (light) and .dark (dark). Values use oklch, but any valid CSS color works.

app/globals.css
/* globals.css — override Sapa's rich colors */
:root {
  --sapa-success: oklch(0.62 0.17 149);
  --sapa-error: oklch(0.58 0.22 27);
  --sapa-warning: oklch(0.75 0.16 78);
  --sapa-info: oklch(0.6 0.13 240);
}

.dark {
  --sapa-success: oklch(0.7 0.16 150);
  --sapa-error: oklch(0.7 0.19 22);
  --sapa-warning: oklch(0.82 0.16 82);
  --sapa-info: oklch(0.68 0.14 240);
}

The result — flip the theme to see both palettes: