Toast
The toast component is used to give feedback to users after an action has taken place.
Features
- Support for screen readers.
- Limit the number of visible toasts.
- Manage promises within toast.
- Pause on hover, focus or page idle.
- Can remove or update toast programmatically.
Installation
To use the toast machine in your project, run the following command in your command line:
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
This command will install the framework agnostic toast logic and the reactive utilities for your framework of choice.
Anatomy
To set up the toast correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the toast package into your project
import * as toast from "@zag-js/toast"
Next, import the required hooks and functions for your framework and use the toast machine in your project 🔥
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 service = useMachine(toast.group.machine, { id: "1", 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>
The use the toast effectively, you need to understand these key aspects:
Toast Group
-
toast.group.machine
— The state machine representation of a group of toasts. It is responsible for spawning, updating and removing toasts. -
toast.group.connect
— function gives you access to methods you can use to add, update, and remove a toast.We recommend setting up the toast group machine once at the root of your project.
Toast Item
toast.machine
— The state machine representation of a single toast.toast.connect
— The function that takes the toast machine and returns methods and JSX properties.
Creating a toast
There are five toast types that can be created with the toast machine. info
,
success
, loading
, custom
and error
.
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
,loading
, orcustom
.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.
Changing the placement
Use the placement
property when you call the toaster.create(...)
to change
the position of the toast.
toaster.info({ title: "Hello World", description: "This is a toast", 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, })
When using overlap, the toast's placement must match the placement
of the
toast group (which is bottom
by default).
Be sure to set up the required styles to make the toasts overlap 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, })
You can also use the
toaster.upsert(...)
function which creates or updates a toast.
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 service = useMachine(toast.group.machine, ({ id: "1", 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
id
passed intotoaster.create(...)
or, - the returned random
id
when thetoaster.create(...)
is called.
You can use any of the following methods to control a toast:
toaster.upsert(...)
— Creates or updates 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.
// grab the id from the created toast const id = toaster.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, placement: "top-start", }) // 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)
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.
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", }), })
Pausing the toasts
There are three scenarios we provide to pause a toast from timing out:
- When the document loses focus or the page is idle (e.g. switching to a new
browser tab), controlled via the
pauseOnPageIdle
context property. - When the
toaster.pause(id)
is called.
// Global pause options const service = useMachine(toast.group.machine, { pauseOnPageIdle: true, }) // Programmatically pause a toast (by `id`) // `id` is the return value of `api.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 the number of visible toasts, pass the max
property to
the group machine's context.
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
context property to change the underlying hotkey.
const service = useMachine(toast.group.machine, { 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 rendereddismissed
- The toast is visually invisible 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" | "dismissed" | "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 the gap
context property.
const service = useMachine(toast.group.machine, { gap: 24, })
Changing the offset
The toast region has a default 16px
offset from the viewport. Use the offset
context property to change the offset.
const service = useMachine(toast.group.machine, { 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 it transition smoothly, you should includes 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
() => number
The total number of toastsgetToasts
() => ToastProps<any>[]
The toastssubscribe
(callback: (toasts: Options<O>[]) => void) => VoidFunction
Subscribe to the toast group
Data Attributes
Edit this page on GitHub