Toast
Toast provides brief feedback after an action.
Features
- Supports screen readers
- Limits the number of visible toasts
- Handles promise lifecycles
- Pauses on hover, focus, or page idle
- Supports programmatic update/remove
Installation
Install the toast package:
npm install @zag-js/toast @zag-js/react # or yarn add @zag-js/toast @zag-js/react
npm install @zag-js/toast @zag-js/solid # or yarn add @zag-js/toast @zag-js/solid
npm install @zag-js/toast @zag-js/vue # or yarn add @zag-js/toast @zag-js/vue
npm install @zag-js/toast @zag-js/svelte # or yarn add @zag-js/toast @zag-js/svelte
Anatomy
Check the toast anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the toast package:
import * as toast from "@zag-js/toast"
Then use the framework integration helpers:
import { useMachine, normalizeProps } from "@zag-js/react" import * as toast from "@zag-js/toast" import { useId } from "react" // 1. Create the toast store const toaster = toast.createStore({ overlap: true, placement: "top-end", }) // 2. Design the toast component function Toast(props) { const machineProps = { ...props.toast, parent: props.parent, index: props.index, } const service = useMachine(toast.machine, machineProps) const api = toast.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <h3 {...api.getTitleProps()}>{api.title}</h3> <p {...api.getDescriptionProps()}>{api.description}</p> <button onClick={api.dismiss}>Close</button> </div> ) } // 3. Design the toaster export function Toaster() { const service = useMachine(toast.group.machine, { id: useId(), store: toaster, }) const api = toast.group.connect(service, normalizeProps) return ( <div {...api.getGroupProps()}> {api.getToasts().map((toast, index) => ( <Toast key={toast.id} toast={toast} parent={service} index={index} /> ))} </div> ) } // 4. Render the toaster in your app export function App() { return ( <> <Toaster /> <ExampleComponent /> </> ) } // 5. Within your app function Demo() { return ( <div> <button onClick={() => { toaster.create({ title: "Hello" }) }} > Info toast </button> <button onClick={() => { toaster.create({ title: "Data submitted!", type: "success" }) }} > Success toast </button> </div> ) }
import { useMachine, normalizeProps, Key } from "@zag-js/solid" import * as toast from "@zag-js/toast" import { createMemo, createUniqueId, createSignal, createContext, useContext, For, } from "solid-js" // 1. Create the toast store const toaster = toast.createStore({ placement: "top-end", overlap: true, }) // 2. Design the toast component function Toast(props) { const machineProps = createMemo(() => ({ ...props.toast(), parent: props.parent, index: props.index(), })) const service = useMachine(toast.machine, machineProps) const api = createMemo(() => toast.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <h3 {...api().getTitleProps()}>{api().title}</h3> <p {...api().getDescriptionProps()}>{api().description}</p> <button onClick={api().dismiss}>Close</button> </div> ) } // 3. Design the toaster export function Toaster() { const service = useMachine(toast.group.machine, { id: createUniqueId(), store: toaster, }) const api = createMemo(() => toast.group.connect(service, normalizeProps)) return ( <div {...api().getGroupProps()}> <Key each={api().getToasts()} by={(toast) => toast.id}> {(toast, index) => ( <Toast toast={toast} parent={service} index={index} /> )} </Key> </div> ) } // 4. Render the toaster in your app export function App() { return ( <> <Toaster /> <ExampleComponent /> </> ) } // 5. Within your app function Demo() { return ( <div> <button onClick={() => { toaster.create({ title: "Hello" }) }} > Info toast </button> <button onClick={() => { toaster.create({ title: "Data submitted!", type: "success" }) }} > Success toast </button> </div> ) }
<script setup> import * as toast from "@zag-js/toast" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" // 1. Create the toaster const toaster = toast.createStore({ placement: "top-end", overlap: true, }) </script> <script setup> // 2. Design the toast component const props = defineProps<{ toast: toast.Options, index: number, parent: toast.GroupService }>() const machineProps = computed(() => ({ ...props.toast, parent: props.parent, index: props.index })) const service = useMachine(toast.machine, machineProps) const api = computed(() => toast.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <h3 v-bind="api.getTitleProps()">{{ api.title }}</h3> <p v-bind="api.getDescriptionProps()">{{ api.description }}</p> <button @click="api.dismiss()">Close</button> </div> </template> <script setup> // 3. Design the toaster const service = useMachine(toast.group.machine, { id: "1", store: toaster }) const api = toast.group.connect(service, normalizeProps) </script> <template> <div v-bind="api.getGroupProps()"> <Toast v-for="toast in api.getToasts()" :key="toast.id" :toast="toast" :index="index" :parent="service" /> </div> <RestOfYourApp /> </template> <script setup> // 4. Within your app const topRightToast = () => toast.create({ title: "Hello" }) const bottomRightToast = () => toast.create({ title: "Data submitted!", type: "success" }) </script> <template> <button @click="topRightToast">Add top-right toast</button> <button @click="bottomRightToast">Add bottom-right toast</button> </template>
<script lang="ts"> import { normalizeProps, useMachine } from "@zag-js/svelte" import * as toast from "@zag-js/toast" // 1. Create the single toast const toaster = toast.createStore({ placement: "top-end", overlap: true, }) // 2. Design the toast component interface ToastProps { toast: toast.Options index: number parent: toast.GroupService } const { toast, index, parent }: ToastProps = $props() const machineProps = $derived({ ...toast, parent, index }) const service = useMachine(toast.machine, () => machineProps) const api = $derived(toast.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <h3 {...api.getTitleProps()}>{api.title}</h3> <p {...api.getDescriptionProps()}>{api.description}</p> <button onclick={api.dismiss}>Close</button> </div> <!-- 3. Design the toaster --> <script lang="ts"> import { normalizeProps, useMachine } from "@zag-js/svelte" import * as toast from "@zag-js/toast" import Toast from "./toast-item.svelte" const id = $props.id() const service = useMachine(toast.group.machine, { id, store: toaster, }) const api = $derived(toast.group.connect(service, normalizeProps)) </script> <div {...api.getGroupProps()}> {#each api.getToasts() as toast, index (toast.id)} <Toast toast={toast} index={index} parent={service} /> {/each} </div> <!-- 4. Wrap your app with the toaster --> <script lang="ts"> import Toaster from "./toaster.svelte" </script> <Toaster /> <!-- 5. Within your app --> <div> <button onclick={() => { toaster.create({ title: "Hello" }) }}> Add top-right toast </button> <button onclick={() => { toaster.create({ title: "Data submitted!", type: "success" }) }}> Success toast </button> </div>
To use toast effectively, understand these key parts:
Toast Group
-
toast.group.machine- State machine logic for the toast region. -
toast.group.connect- Maps group state to JSX props and subscriptions.We recommend setting up the toast group machine once at the root of your project.
Toast Item
toast.machine- State machine logic for a single toast.toast.connect- Maps toast state to JSX props and controls.
Creating a toast
Common toast types are info, success, warning, loading, and error. You
can also pass a custom type string.
Helper methods are also available: toaster.info(...), toaster.success(...),
toaster.warning(...), toaster.error(...), and toaster.loading(...).
To create a toast, use the toaster.create(...) method.
toaster.create({ title: "Hello World", description: "This is a toast", type: "info", })
The options you can pass in are:
title— The title of the toast.description— The description of the toast.type— The type of the toast. Can be eithererror,success,info,warning,loading, or any custom string.duration— The duration of the toast. The default duration is computed based on the specifiedtype.onStatusChange— A callback that listens for the status changes across the toast lifecycle.removeDelay— The delay before unmounting the toast from the DOM. Useful for transition.action— Optional action button withlabelandonClick.closable— Whether to render a close trigger.
Changing the placement
Placement is configured on the toast store and applies to the whole toast group.
const toaster = toast.createStore({ placement: "top-start", })
Overlapping toasts
When multiple toasts are created, they are rendered in a stack. To make the
toasts overlap, set the overlap property to true.
const toaster = toast.createStore({ overlap: true, })
Be sure to set up the required styles to make overlap work correctly.
Changing the duration
Every toast has a default visible duration depending on the type set. Here's
the following toast types and matching default durations:
| type | duration |
|---|---|
info | 5000 |
error | 5000 |
success | 2000 |
loading | Infinity |
You can override the duration of the toast by passing the duration property to
the toaster.create(...) function.
toaster.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, })
Use toaster.create(...) for new toasts and toaster.update(id, ...) to modify
an existing one.
Using portals
Using a portal is helpful to ensure that the toast is rendered outside the DOM
hierarchy of the parent component. To render the toast in a portal, wrap the
rendered toasts in the ToastProvider within your framework-specific portal.
import { useMachine, normalizeProps, Portal } from "@zag-js/react" import * as toast from "@zag-js/toast" // ... // 3. Create the toast group provider, wrap your app with it export function Toaster() { const service = useMachine(toast.group.machine, { id: "1", store: toaster }) const api = toast.group.connect(service, normalizeProps) return ( <Portal> {api.getToasts().map((toast, index) => ( <Toast key={toast.id} actor={toast} parent={service} index={index} /> ))} </Portal> ) }
<script lang="ts"> import { portal } from "@zag-js/svelte" // ... const id = $props.id() const service = useMachine(toast.group.machine, ({ id, store: toaster })) const api = $derived(toast.group.connect(service, normalizeProps)) // ... </script> <div use:portal {...api.getGroupProps()}> {#each api.getToasts() as toast, index (toast.id)} <Toast toast={toast} index={index} parent={service} /> {/each} </div>
Programmatic control
To update a toast programmatically, you need access to the unique identifier of the toast.
This identifier can be either:
- the
idpassed intotoaster.create(...)or, - the returned random
idwhen thetoaster.create(...)is called.
You can use any of the following methods to control a toast:
toaster.update(...)— Updates a toast.toaster.remove(...)— Removes a toast instantly without delay.toaster.dismiss(...)— Removes a toast with delay.toaster.pause(...)— Pauses a toast.toaster.resume(...)— Resumes a toast.toaster.isVisible(...)— Checks if a toast is currently visible.toaster.isDismissed(...)— Checks if a toast has been dismissed.
// grab the id from the created toast const id = toaster.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, }) // update the toast toaster.update(id, { title: "Hello World", description: "This is a toast", type: "success", }) // remove the toast toaster.remove(id) // dismiss the toast toaster.dismiss(id)
Pause and resume all toasts
Call pause()/resume() without an id to affect all active toasts.
toaster.pause() toaster.resume()
Handling promises
The toast group API exposes a toaster.promise() function to allow you update
the toast when it resolves or rejects.
With the promise API, you can pass the toast options for each promise lifecycle. The
loadingoption is required
toaster.promise(promise, { loading: { title: "Loading", description: "Please wait...", }, success: (data) => ({ title: "Success", description: "Your request has been completed", }), error: (err) => ({ title: "Error", description: "An error has occurred", }), })
toaster.promise(...) returns { id, unwrap }, so you can await the original
result:
const result = toaster.promise(fetchData(), { loading: { title: "Loading..." }, }) await result?.unwrap()
Pausing the toasts
There are three scenarios that pause toast timeout:
- When a user hovers or focuses the toast region.
- When the document loses focus or the page is idle (e.g. switching to a new
browser tab), controlled via the
pauseOnPageIdlestore option. - When the
toaster.pause(id)is called.
// Global pause options const toaster = toast.createStore({ pauseOnPageIdle: true, }) // Programmatically pause a toast (by `id`) // `id` is the return value of `toaster.create(...)` toaster.pause(id)
Limiting the number of toasts
Toasts are great but displaying too many of them can sometimes hamper the user
experience. To limit visible toasts, set max on the store.
const toaster = toast.createStore({ max: 10, })
Focus Hotkey for toasts
When a toast is created, you can focus the toast region by pressing the
alt + T. This is useful for screen readers and keyboard navigation.
Set the hotkey store option to change the underlying hotkey.
const service = useMachine(toast.group.machine, { store: toast.createStore({ hotkey: ["F6"] }), })
Listening for toast lifecycle
When a toast is created, you can listen for the status changes across its
lifecycle using the onStatusChange callback when you call
toaster.create(...).
The status values are:
visible- The toast is mounted and rendereddismissing- The toast is closing but still mountedunmounted- The toast has been completely unmounted and no longer exists
toaster.info({ title: "Hello World", description: "This is a toast", type: "info", onStatusChange: (details) => { // details => { status: "visible" | "dismissing" | "unmounted" } console.log("Toast status:", details) }, })
Changing the gap between toasts
When multiple toasts are rendered, a gap of 16px is applied between each
toast. To change this value, set gap on the store.
const toaster = toast.createStore({ gap: 24, })
Changing the offset
The toast region has a default 1rem offset from the viewport. Use the
offsets store option to change this offset.
const toaster = toast.createStore({ offsets: "24px", })
Styling guide
Requirement
The toast machine injects a bunch of css variables that are required for it to work. You need to connect these variables in your styles.
[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; }
To make transitions smooth, include transition properties.
[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); }
Toast styling
When a toast is created and the api.getRootProps() from the toast.connect is
used, the toast will have a data-type that matches the specified type at its
creation.
You can use this property to style the toast.
[data-part="root"][data-type="info"] { /* Styles for the specific toast type */ } [data-part="root"][data-type="error"] { /* Styles for the error toast type */ } [data-part="root"][data-type="success"] { /* Styles for the success toast type */ } [data-part="root"][data-type="loading"] { /* Styles for the loading toast type */ }
Methods and Properties
Machine API
The toast's api exposes the following methods:
getCount() => numberThe total number of toastsgetToasts() => ToastProps<any>[]The toastssubscribe(callback: (toasts: Options<O>[]) => void) => VoidFunctionSubscribe to the toast group