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/vue # or yarn add @zag-js/toast @zag-js/vue
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 { useActor, useMachine, normalizeProps } from "@zag-js/react" import * as toast from "@zag-js/toast" import { createContext } from "react" // 1. Create the single toast function Toast(props) { const [state, send] = useActor(props.actor) const api = toast.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <h3 {...api.titleProps}>{api.title}</h3> <p {...api.descriptionProps}>{api.description}</p> <button onClick={api.dismiss}>Close</button> </div> ) } // 2. Create the toast context const ToastContext = createContext() const useToast = () => useContext(ToastContext) // 3. Create the toast group provider export function ToastProvider({ children }) { const [state, send] = useMachine(toast.group.machine({ id: "1" })) const api = toast.group.connect(state, send, normalizeProps) return ( <ToastContext.Provider value={api}> {Object.entries(api.toastsByPlacement).map(([placement, toasts]) => ( <div key={placement} {...api.getGroupProps({ placement })}> {toasts.map((toast) => ( <Toast key={toast.id} actor={toast} /> ))} </div> ))} {children} </ToastContext.Provider> ) } // 4. Wrap your app with the toast group provider export function App() { return ( <ToastProvider> <ExampleComponent /> </ToastProvider> ) } // 4. Within your app function ExampleComponent() { const toast = useToast() return ( <div> <button onClick={() => { toast.create({ title: "Hello", placement: "top-right" }) }} > Add top-right toast </button> <button onClick={() => { toast.create({ title: "Data submitted!", type: "success", placement: "bottom-right", }) }} > Add bottom-right toast </button> </div> ) }
import { useActor, useMachine, normalizeProps } from "@zag-js/solid" import * as toast from "@zag-js/toast" import { createMemo, createUniqueId, createSignal, createContext, useContext, For } from "solid-js" // 1. Create the single toast function Toast(props) { const [state, send] = useActor(props.actor) const api = createMemo(() => toast.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <h3 {...api().titleProps}>{api().title}</h3> <p {...api().descriptionProps}>{api().description}</p> <button onClick={api().dismiss}>Close</button> </div> ) } // 2. Create the toast context const ToastContext = createContext() const useToast = () => useContext(ToastContext) // 3. Create the toast group provider export function ToastProvider(props) { const [state, send] = useMachine(toast.group.machine({ id: createUniqueId() })) const api = createMemo(() => toast.group.connect(state, send, normalizeProps)) return ( <ToastContext.Provider value={api}> <For each={Object.entries(api().toastsByPlacement)}> {([placement, toasts]) => ( <div key={placement} {...api().getGroupProps({ placement })}> <For each={toasts}> {(toast) => ( <Toast key={toast.id} actor={toast} /> )} </For> </div> )} </For> {props.children} </ToastContext.Provider> ) } // 4. Wrap your app with the toast group provider export function App() { return ( <ToastProvider> <ExampleComponent /> </ToastProvider> ) } // 4. Within your app function ExampleComponent() { const toast = useToast() return ( <div> <button onClick={() => { toast().create({ title: "Hello", placement: "top-right" }) }} > Add top-right toast </button> <button onClick={() => { toast.create({ title: "Data submitted!", type: "success", placement: "bottom-right", }) }} > Add bottom-right toast </button> </div> ) }
import * as toast from "@zag-js/toast" import { normalizeProps, useActor, useMachine } from "@zag-js/vue" import { computed, defineComponent, inject, reactive } from "vue" // 1. Create the single toast const Toast = defineComponent({ setup(props) { const [state, send] = useActor(props.actor) const apiRef = computed(() => toast.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <h3 {...api.titleProps}>{api.title}</h3> <p {...api.descriptionProps}>{api.description}</p> <button onClick={api.dismiss}>Close</button> </div> ) } }, }) // 2. Add toast to your global context so it can be called anywhere in the app const [state, send] = useMachine(toast.group.machine({ id: "1" })) const toastApi = toast.group.connect(state, send, normalizeProps) // E.g. Store export const store = reactive({ toast: toastApi, }) // Maybe Vue app provide / inject VueApp.provide("toast", toastApi) // 3. Register the toast placements in the root of your app const App = defineComponent({ name: "App", setup() { //Get toast from global context; you could also be getting from a store, // this example uses the Vue app provide method const $toastRef = computed(() => inject("toast")) return () => { const api = $toastRef.value return ( <> {Object.entries(api.toastsByPlacement).map(([placement, toasts]) => ( <div key={placement} {...api.getGroupProps({ placement })}> {toasts.map((toast) => ( <Toast key={toast.id} actor={toast} /> ))} </div> ))} <RestOfYourApp /> </> ) } }, }) export default App // 4. Within your app defineComponent({ name: "Component", setup() { const $toastRef = computed(() => inject("toast")) return () => { const toast = $toastRef.value return ( <div> <button onClick={() => { toast.create({ title: "Hello", placement: "top-right" }) }} > Add top-right toast </button> <button onClick={() => { toast.create({ title: "Data submitted!", type: "success", placement: "bottom-right", }) }} > Add bottom-right toast </button> </div> ) } }, })
<script setup> //imports import * as toast from "@zag-js/toast" import { normalizeProps, useActor, useMachine } from "@zag-js/vue" import { computed, inject, reactive } from "vue" </script> <script setup> // 1. Create the single toast const props = defineProps<{ actor: string}>() const [state, send] = useActor(props.actor) const api = computed(() => toast.connect(state.value, send, normalizeProps)) </script> <template> <div v-bind="api.rootProps"> <h3 v-bind="api.titleProps">{{ api.title }}</h3> <p v-bind="api.descriptionProps">{{ api.description }}</p> <button @click="api.dismiss()">Close</button> </div> </template> <script setup> // 2. Add toast to your global context so it can be called anywhere in the app const [state, send] = useMachine(toast.group.machine({ id: "1" })) const toastApi = toast.group.connect(state.value, send, normalizeProps) // E.g. Store export const store = reactive({ toast: toastApi, }) // Maybe Vue app provide / inject VueApp.provide("toast", toastApi) </script> <script setup> // 3. Register the toast placements in the root of your app // Get toast from global context; you could also be getting from a store, // this example uses the Vue app provide method const api = computed(() => inject("toast")) </script> <template> <div v-for="(toasts, placement, index) in api.toastsByPlacement"> <div :key="placement" v-bind="api.getGroupProps({ placement })" > <Toast v-for="toast in toasts" :key="toast.id" :actor="toast" /> </div> </div> <RestOfYourApp /> </template> <script setup> // 4. Within your app // Get toast from global context; you could also be getting from a store, // this example uses the Vue app provide method const toast = computed(() => inject("toast")) const topRightToast = () => toast.create({ title: 'Hello', placement: 'top-right' }) const bottomRightToast = () => toast.create({ title: "Data submitted!", type: "success", placement: "bottom-right", }); </script> <template> <button @click="topRightToast"> Add top-right toast </button> <button @click="bottomRightToast"> Add bottom-right toast </button> </template>
The toast consists of three key aspects:
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.
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.
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 toast.create(...)
method.
toast.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
.onClose
— A function that is called when the toast is closed.placement
— The placement of the toast.render
— A function that returns the toast JSX (Useful for library authors).removeDelay
— The delay before removing the toast from the DOM. This is useful for managing exit animations. It is set to0
by default.
Setting custom duration
Every toast has a default visible duration depending on the type
set. Here's
the table showing the toast type and duration.
type | duration |
---|---|
info | 5000 |
error | 5000 |
success | 2000 |
custom | 5000 |
loading | Infinity |
You can override the duration of the toast by passing the duration
property to
the toast.create(...)
function.
toast.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, })
You can also use the
toast.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 { Portal } from "@zag-js/react" // ... // 3. Create the toast group provider, wrap your app with it export function ToastProvider() { const [state, send] = useMachine(toast.group.machine({ id: "1" })) const api = toast.group.connect(state, send, normalizeProps) return ( <ToastContext.Provider value={api}> <Portal> {Object.entries(api.toastsByPlacement).map(([placement, toasts]) => ( <div key={placement} {...api.getGroupProps({ placement })}> {toasts.map((toast) => ( <Toast key={toast.id} actor={toast} /> ))} </div> ))} </Portal> </ToastContext.Provider> ) }
Positioning toast
To position a toast when created, you can pass the placement
property when you
call the toast.create(...)
methods provided by the toast group's connect
funtion.
toast.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, placement: "top-start", })
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 intotoast.create(...)
or, - the returned random
id
when thetoast.create(...)
is called.
You can use any of the following methods to control a toast:
toast.upsert(...)
— Creates or updates a toast.toast.update(...)
— Updates a toast.toast.remove(...)
— Removes a toast instantly without delay.toast.dismiss(...)
— Removes a toast with delay.toast.pause(...)
— Pauses a toast.toast.resume(...)
— Resumes a toast.
// grab the id from the created toast const id = toast.create({ title: "Hello World", description: "This is a toast", type: "info", duration: 6000, placement: "top-start", }) // update the toast toast.update(id, { title: "Hello World", description: "This is a toast", type: "success", }) // remove the toast toast.remove(id) // dismiss the toast toast.dismiss(id)
Handling promises
The toast group API exposes a toast.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.
toast.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 toast is hovered or focused, controlled by the
pauseOnInteraction
context property. - When the
toast.pause(id)
provided by thetoast.group.connect(...)
is called.
// Global pause options const [state, send] = useMachine( toast.group.machine({ pauseOnPageIdle: true, pauseOnInteraction: true, }), ) // Programmatically pause a toast (by `id`) // `id` is the return value of `api.create(...)` toast.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 [state, send] = useMachine( toast.group.machine({ max: 10, }), )
Using toast outside component
The toast module exports an api
function you can invoke to get access to
specific methods from the toast.group.connect
.
The methods available are count
, isVisible
, upsert
, dismiss
, remove
,
promise
.
For toast to work outside component, ensure you've setup the toast group as defined in the usage guide
import * as toast from "@zag-js/toast" function fetchData() { const promise = fetch("/api/data").then((res) => res.json()) toast.api().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", }), }) }
Changing the space between toasts
When multiple toasts are rendered, a gutter of 1rem
is applied between each
toast. To change this value, pass the gutter
context property.
const [state, send] = useMachine( toast.group.machine({ gutter: "50px", }), )
Changing the viewport offsets
The toast group's container has position: fixed
css property applied to it
based on the placement set. To change the viewport offsets of the toast group's
container, pass the offsets
context property.
const [state, send] = useMachine( toast.group.machine({ offsets: { top: "10px", right: "10px", bottom: "10px", left: "10px", }, }), )
Custom render functions
When using the toast within a design system, you might want consumers to provide custom render functions that return the toast's JSX element.
Here's how to achieve that:
// 1. Update toast to support custom render function ToastItem(props) { const [state, send] = useActor(props.actor) const api = toast.connect(state, send) // Custom toast JSX defined when `toast.create({ render: () => {...} })` is called const jsx = state.context.render?.(api) // User controlled JSX if (jsx) { return jsx } // Default JSX return ( <div {...api.rootProps}> <p {...api.titleProps}>{api.title}</p> <p {...api.descriptionProps}>{api.description}</p> <p>{api.type === "loading" ? <BeatLoader /> : null}</p> <button onClick={api.dismiss}>Close</button> </div> ) } // 2. Later in the app, provide custom JSX toast.create({ title: "Welcome", description: "This is a toast", type: "info", render(api) { return ( <div style={{ background: api.type === "loading" ? "pink" : "red" }}> <p>{props.title}</p> <p>{props.description}</p> <button onClick={props.dismiss}>Close</button> </div> ) }, })
Styling guide
Toast styling
When a toast is created and the api.rootProps
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="custom"] { /* Styles for the custom toast type */ } [data-part="root"][data-type="loading"] { /* Styles for the loading toast type */ }
Open and close state
When the toast is open or closed, we attach a data-state
attribute to the
toast's root. A common use-case for this is to specify the entry and exit
animations for the toast.
Remember to set the
removeDelay
to a value that matches the exit animation duration.
[data-part="root"][data-state="open"] { animation-name: fadein; animation-fill-mode: forwards; animation-duration: 0.2s; } [data-part="root"][data-state="closed"] { animation-duration: 0.3s; animation-name: fadeout; }
Methods and Properties
The toast's api
exposes the following methods:
Edit this page on GitHub