Skip to content
Suluu

Toaster

A deck rather than a list. New toasts land in front and older ones recede behind them; the idle stack peeks four, hover or focus springs the front three apart, the rest of the deck scrolls into view, and leaving gathers them back into the peek. The time left unwinds as a hairline ring around the icon.

Preview

Fire a few, or stack six, then hover and scroll

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/toast.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

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.

Registry
// 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." })
npm
// 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.

Variants
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.

Actions
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.

Icons
// 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.

Scoped
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.

PropTypeDefaultDescription
titleReactNodeFirst argument. The one line that always shows.
descriptionReactNodeSecondary line under the title.
variant"default" | "success" | "error" | "warning" | "info""default"Chooses the icon and its tint. The surface never changes color.
durationnumberToaster's durationMilliseconds on screen. Use Infinity to keep it until dismissed.
action{ label, onClick }Inline button. Runs, then dismisses the toast.
iconReactNodevariant iconReplaces the icon for this toast.
onClose(id) => voidRuns on every dismissal path, including toast.dismiss().
motionIntensity"subtle" | "default" | "expressive"Toaster's intensityOverrides the spring character for this toast.

Toaster props

Defaults for the whole deck. Every toast can override duration and motionIntensity for itself.

PropTypeDefaultDescription
position"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right""bottom-right"Corner the deck grows from.
durationnumber4500Default milliseconds on screen.
maxnumber8How 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.
iconsPartial<Record<Variant, ReactNode>>Replaces the default icon for a whole variant.
containerHTMLElement | nulldocument.bodyPortal target. null waits until a host is ready; omit to use document.body.
labelstring"Notifications"Accessible name of the landmark.
classNamestringClass 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.

↑↓ to navigate↵ to openesc to close