Installation
Registry — recommended
Copies the component into your configured shadcn UI directory and installs Motion.
npx shadcn@latest add https://suluu.site/r/toast.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
Render one <Toaster /> near the root of your app. State lives in a module-level store, not in React context, so toast() works from event handlers, effects, and plain functions alike — no provider and no hook.
// app/layout.tsx
import { Toaster } from "@/components/ui/toast"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}
// anywhere else
import { toast } from "@/components/ui/toast"
toast.success("Draft saved", { description: "Synced a moment ago." })// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";
// app/layout.tsx
import { Toaster } from "suluu/toast"
<Toaster position="top-center" duration={6000} />
// anywhere else
import { toast } from "suluu/toast"
toast.error("Upload failed")Variants
The surface stays the same neutral in every variant. Only the icon and its ring take a tint, so a burst of errors never turns the corner of your app into a wall of red.
toast("Note moved to trash")
toast.success("Draft saved")
toast.error("Upload failed")
toast.warning("Storage almost full")
toast.info("2 people joined the workspace")Actions and duration
An action dismisses its toast once it runs. Every call returns an id you can pass to toast.dismiss(), and toast.dismiss() with no argument clears the deck.
toast("Note moved to trash", {
action: { label: "Undo", onClick: restoreNote },
})
// Keep one on screen until something else resolves it.
const id = toast.info("Syncing your library", { duration: Infinity })
await sync()
toast.dismiss(id)Custom icons
The defaults are inline SVGs that draw themselves on as the toast settles — no icon library involved. Replace one toast's icon or a whole variant's.
// One toast
toast.success("Deployed", { icon: <RocketIcon /> })
// Every toast of a variant
<Toaster icons={{ success: <RocketIcon /> }} />A second deck
createToaster() returns an independent store, toast, and Toaster. Paired with container it keeps a deck inside one panel — which is exactly how the previews on this page stay in their boxes. Pass the host from a callback ref; null waits to portal instead of flashing onto the page.
import { useState } from "react"
import { createToaster } from "@/components/ui/toast"
// An independent queue and viewport, for a modal, a canvas, or a test.
const { Toaster: PanelToaster, toast: panelToast } = createToaster()
const [host, setHost] = useState<HTMLElement | null>(null)
<div className="relative" ref={setHost}>
<PanelToaster container={host} />
</div>In context
Changing a setting confirms itself, and the confirmation carries the way back.
Visibility
Changing this takes effect immediately.
Props
Options for toast(title, options) and every variant helper.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | First argument. The one line that always shows. |
| description | ReactNode | — | Secondary line under the title. |
| variant | "default" | "success" | "error" | "warning" | "info" | "default" | Chooses the icon and its tint. The surface never changes color. |
| duration | number | Toaster's duration | Milliseconds on screen. Use Infinity to keep it until dismissed. |
| action | { label, onClick } | — | Inline button. Runs, then dismisses the toast. |
| icon | ReactNode | variant icon | Replaces the icon for this toast. |
| onClose | (id) => void | — | Runs on every dismissal path, including toast.dismiss(). |
| motionIntensity | "subtle" | "default" | "expressive" | Toaster's intensity | Overrides the spring character for this toast. |
Toaster props
Defaults for the whole deck. Every toast can override duration and motionIntensity for itself.
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "bottom-right" | Corner the deck grows from. |
| duration | number | 4500 | Default milliseconds on screen. |
| max | number | 8 | How many live in the deck. The rest wait their turn. The collapsed stack peeks four; hover or focus expands the front three, and the rest of the deck scrolls. |
| motionIntensity | "subtle" | "default" | "expressive" | "default" | Spring character of the stack, enter, and exit. |
| icons | Partial<Record<Variant, ReactNode>> | — | Replaces the default icon for a whole variant. |
| container | HTMLElement | null | document.body | Portal target. null waits until a host is ready; omit to use document.body. |
| label | string | "Notifications" | Accessible name of the landmark. |
| className | string | — | Class name applied to the deck. |
Theming
Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.
--suluu-toast-surfaceToast background, translucent by default.--suluu-toast-foregroundTitle color.--suluu-toast-mutedDescription and close button color.--suluu-toast-borderHairline around the surface.--suluu-toast-shadowSurface depth, including the top highlight.--suluu-toast-ringKeyboard focus ring.--suluu-toast-offsetColor behind the focus ring offset.--suluu-toast-trackUnfilled part of the countdown ring.--suluu-toast-actionAction and close button surface.--suluu-toast-action-foregroundAction button label.--suluu-toast-action-hoverAction and close button hover surface.--suluu-toast-neutralIcon tint for the default variant.--suluu-toast-successIcon and ring tint for success.--suluu-toast-errorIcon and ring tint for error.--suluu-toast-warningIcon and ring tint for warning.--suluu-toast-infoIcon and ring tint for info.The deck exposes data-expanded while it is hovered, keyboard-focused, or being dragged, data-scrollable while more than three toasts are expanded, and each toast carries data-variant.
Accessibility
The deck is a named landmark holding a list, so it is somewhere a screen reader can return to. Success, info, and plain toasts announce as role="status" politely; errors and warnings use role="alert" and interrupt.
Every toast carries a close button that is always focusable. Pointer devices that can hover only see it on hover or keyboard focus; touch devices keep it visible. Keyboard focus (focus-visible) inside the deck springs it open, so a keyboard user never reaches a control hidden behind the front card, and it holds every countdown until that focus leaves. A pointer click does not keep the deck expanded or the timers paused once the pointer leaves. Escape dismisses the toast that contains focus. The collapsed stack peeks four; anything deeper stays in the deck and is paused until it is promoted. Hover or keyboard focus expands the front three; the rest of the live deck is a short scroll away. Leaving hover or keyboard focus squares the deck back into the peek on the pinned edge.
Countdowns also pause while the deck is hovered or the tab is in the background. With reduced motion the stack settles instantly and toasts cross-fade instead of travelling; the countdown ring stays, because it carries information rather than motion.