Skip to main content
Toast provides brief, non-blocking feedback after an action. Toasts appear in a region, are announced to screen readers, and disappear automatically after a timeout. Features
  • Screen reader announcements via ARIA live region
  • Limits the number of visible toasts
  • Promise lifecycle handling (loading → success/error)
  • Pauses on hover, focus, or when the page is idle
  • Programmatic create, update, pause, resume, and dismiss
  • Configurable placement, gap, offset, and overlap modes

Installation

npm install @zag-js/toast @zag-js/react

Usage

The toast system uses two separate machines: a toast group machine that manages the region, and a toast item machine for each individual notification.

Step 1: Create a store

Create a shared store (usually at the root of your app) to hold toast state.
import * as toast from "@zag-js/toast"

export const toaster = toast.createStore({
  placement: "bottom-end",
  max: 5,
})

Step 2: Render the toast group

import { useMachine, normalizeProps } from "@zag-js/react"
import * as toast from "@zag-js/toast"
import { useId } from "react"
import { toaster } from "./toaster"

function Toast({ actor }) {
  const service = useMachine(actor)
  const api = toast.connect(service, normalizeProps)

  return (
    <div {...api.getRootProps()}>
      {api.title && <p {...api.getTitleProps()}>{api.title}</p>}
      {api.description && (
        <p {...api.getDescriptionProps()}>{api.description}</p>
      )}
      <button {...api.getCloseTriggerProps()}>✕</button>
    </div>
  )
}

function ToastProvider() {
  const service = useMachine(toast.group.machine, {
    id: useId(),
    store: toaster,
  })
  const api = toast.group.connect(service, normalizeProps)

  return (
    <div {...api.getGroupProps()}>
      {api.getToasts().map((actor) => (
        <Toast key={actor.id} actor={actor} />
      ))}
    </div>
  )
}

// Mount <ToastProvider /> once at the root of your app.

Step 3: Create toasts from anywhere

Use the toaster store to trigger notifications from anywhere in your app.
import { toaster } from "./toaster"

// Typed shorthand methods
toaster.success({ title: "Saved!", description: "Your changes were saved." })
toaster.error({ title: "Error", description: "Something went wrong." })
toaster.info({ title: "Info", description: "Your session expires soon." })
toaster.warning({ title: "Warning", description: "Disk space is low." })
toaster.loading({ title: "Uploading…" })

// Generic create
toaster.create({
  title: "Hello",
  description: "This is a notification",
  type: "info",
})

Placement

Configure placement on the store. Applies to the entire toast group.
const toaster = toast.createStore({
  placement: "top-end", // "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end"
})

Overlapping toasts

Enable overlap to stack toasts on top of each other (Sonner-style) instead of in a list.
const toaster = toast.createStore({
  overlap: true,
})
Overlapping mode requires the CSS variable bindings on [data-part="root"]. See the Styling section.

Duration

Default durations by toast type:
TypeDefault duration
info5000 ms
error5000 ms
success2000 ms
warning5000 ms
loadingInfinity
Override the duration per toast:
toaster.create({
  title: "Custom duration",
  type: "info",
  duration: 8000,
})

Promise toasts

Use toaster.promise() to automatically track a promise’s loading, success, and error states.
toaster.promise(fetchData(), {
  loading: {
    title: "Loading",
    description: "Please wait…",
  },
  success: (data) => ({
    title: "Done",
    description: `Loaded ${data.count} items`,
  }),
  error: (err) => ({
    title: "Failed",
    description: err.message,
  }),
})
toaster.promise() returns { id, unwrap } so you can await the original result:
const result = toaster.promise(fetchData(), { loading: { title: "Loading…" } })
const data = await result?.unwrap()

Programmatic control

// Capture the id returned by create
const id = toaster.create({ title: "Saving…", type: "loading" })

// Update a toast (e.g., after the operation completes)
toaster.update(id, { title: "Saved!", type: "success" })

// Remove instantly (no dismiss delay)
toaster.remove(id)

// Dismiss with delay (allows exit transition)
toaster.dismiss(id)

// Pause and resume
toaster.pause(id)     // pause a specific toast
toaster.resume(id)    // resume a specific toast
toaster.pause()       // pause all toasts
toaster.resume()      // resume all toasts

// Visibility checks
toaster.isVisible(id)    // => boolean
toaster.isDismissed(id)  // => boolean

Pausing toasts

Toasts pause automatically when:
  • A user hovers over or focuses the toast region
  • The browser tab loses focus or the page becomes idle (configure with pauseOnPageIdle)
const toaster = toast.createStore({
  pauseOnPageIdle: true,
})

Limiting visible toasts

const toaster = toast.createStore({
  max: 5,
})
When the limit is reached, new toasts queue until older ones dismiss.

Toast lifecycle events

Listen for status changes on a per-toast basis:
toaster.create({
  title: "Processing",
  type: "loading",
  onStatusChange: (details) => {
    // details => { status: "visible" | "dismissing" | "unmounted" }
    console.log("Toast status:", details.status)
  },
})

Layout options

const toaster = toast.createStore({
  gap: 24,          // gap between stacked toasts (default: 16)
  offsets: "24px",  // distance from the viewport edge (default: "1rem")
})

Focus hotkey

Press Alt + T to move keyboard focus to the toast region. Change the hotkey with the hotkey store option.
const toaster = toast.createStore({
  hotkey: ["F6"],
})

API reference

placement
"top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end"
The position of the toast group on screen. Defaults to "bottom".
max
number
The maximum number of visible toasts. Extra toasts are queued. Defaults to 24.
overlap
boolean
Whether toasts overlap (stack on top) instead of list below each other.
gap
number
The gap in pixels between toasts. Defaults to 16.
offsets
string | Record<'top' | 'right' | 'bottom' | 'left', string>
The distance from the viewport edge. Defaults to "1rem".
duration
number
A global default duration override for all toasts.
removeDelay
number
How long (ms) to keep the toast mounted after dismiss, to allow exit transitions. Defaults to 200.
pauseOnPageIdle
boolean
Whether to pause toasts when the page is hidden or idle. Defaults to false.
hotkey
string[]
The keyboard shortcut to move focus to the toast region. Defaults to ["altKey", "KeyT"].

Toast options (passed to toaster.create)

title
string
The title of the toast notification.
description
string
The description text of the toast.
type
"info" | "success" | "warning" | "error" | "loading"
The toast type. Controls the default duration and ARIA role.
duration
number
Per-toast duration override in milliseconds.
closable
boolean
Whether to render a close button. Defaults to false.
action
{ label: string, onClick: () => void }
An optional action button to render inside the toast.
onStatusChange
(details: { status: 'visible' | 'dismissing' | 'unmounted' }) => void
Callback for toast lifecycle status changes.

Styling

The toast machine injects CSS variables that control position and visibility. Connect them in your stylesheet:
[data-part="root"] {
  translate: var(--x) var(--y);
  scale: var(--scale);
  z-index: var(--z-index);
  height: var(--height);
  opacity: var(--opacity);
  will-change: translate, opacity, scale;
}

Transition

[data-part="root"] {
  transition:
    translate 400ms,
    scale 400ms,
    opacity 400ms;
  transition-timing-function: cubic-bezier(0.21, 1.02, 0.73, 1);
}

[data-part="root"][data-state="closed"] {
  transition:
    translate 400ms,
    scale 400ms,
    opacity 200ms;
  transition-timing-function: cubic-bezier(0.06, 0.71, 0.55, 1);
}

Styling by type

[data-part="root"][data-type="info"] { /* info toast */ }
[data-part="root"][data-type="success"] { /* success toast */ }
[data-part="root"][data-type="warning"] { /* warning toast */ }
[data-part="root"][data-type="error"] { /* error toast */ }
[data-part="root"][data-type="loading"] { /* loading toast */ }

Build docs developers (and LLMs) love