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
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.
<!-- ToastProvider.vue -->
<script setup>
import { useMachine, normalizeProps } from "@zag-js/vue"
import * as toast from "@zag-js/toast"
import { useId, computed } from "vue"
import { toaster } from "./toaster"
const service = useMachine(toast.group.machine, {
id: useId(),
store: toaster,
})
const api = computed(() => toast.group.connect(service, normalizeProps))
</script>
<template>
<div v-bind="api.getGroupProps()">
<div
v-for="actor in api.getToasts()"
:key="actor.id"
<!-- render individual toasts here -->
/>
</div>
</template>
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:
| Type | Default duration |
|---|
info | 5000 ms |
error | 5000 ms |
success | 2000 ms |
warning | 5000 ms |
loading | Infinity |
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".
The maximum number of visible toasts. Extra toasts are queued. Defaults to 24.
Whether toasts overlap (stack on top) instead of list below each other.
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".
A global default duration override for all toasts.
How long (ms) to keep the toast mounted after dismiss, to allow exit transitions. Defaults to 200.
Whether to pause toasts when the page is hidden or idle. Defaults to false.
The keyboard shortcut to move focus to the toast region. Defaults to ["altKey", "KeyT"].
Toast options (passed to toaster.create)
The title of the toast notification.
The description text of the toast.
type
"info" | "success" | "warning" | "error" | "loading"
The toast type. Controls the default duration and ARIA role.
Per-toast duration override in milliseconds.
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 */ }