useToast
Provides the behavior and accessibility implementation for a toast component. Toasts display brief, temporary notifications of actions, errors, or other events in an application.
install | yarn add react-aria |
---|---|
version | 3.35.2 |
usage | import {useToastRegion, useToast} from 'react-aria' |
API#
useToastRegion<T>(
props: AriaToastRegionProps,
state: ToastState<T>,
ref: RefObject<HTMLElement
| | null>
): ToastRegionAria
useToast<T>(
props: AriaToastProps<T>,
state: ToastState<T>,
ref: RefObject<FocusableElement
| | null>
): ToastAria
Features#
There is no built in way to toast notifications in HTML. useToastRegion
and useToast
help achieve accessible toasts that can be styled as needed.
- Accessible – Toasts follow the ARIA alert pattern. They are rendered in a landmark region, which keyboard and screen reader users can easily jump to when an alert is announced.
- Focus management – When a toast unmounts, focus is moved to the next toast if any. Otherwise, focus is restored to where it was before navigating to the toast region.
- Priority queue – Toasts are displayed according to a priority queue, displaying a configurable number of toasts at a time. The queue can either be owned by a provider component, or global.
- Animations – Toasts support optional entry and exit animations.
Anatomy#
A toast region is an ARIA landmark region labeled "Notifications" by default. A toast region contains one or more visible toasts, in priority order. When the limit is reached, additional toasts are queued until the user dismisses one. Each toast is an ARIA alert element, containing the content of the notification and a close button.
Landmark regions including the toast container can be navigated using the keyboard by pressing the F6 key to move forward, and the Shift + F6 key to move backward. This provides an easy way for keyboard users to jump to the toasts from anywhere in the app. When the last toast is closed, keyboard focus is restored.
useToastRegion
returns props that you should spread onto the toast container element:
Name | Type | Description |
regionProps | DOMAttributes | Props for the landmark region element. |
useToast
returns props that you should spread onto an individual toast and its child elements:
Name | Type | Description |
toastProps | DOMAttributes | Props for the toast container, non-modal dialog element. |
contentProps | DOMAttributes | Props for the toast content alert message. |
titleProps | DOMAttributes | Props for the toast title element. |
descriptionProps | DOMAttributes | Props for the toast description element, if any. |
closeButtonProps | AriaButtonProps | Props for the toast close button. |
Example#
Toasts consist of three components. The first is a ToastProvider
component which will manage the state for the toast queue with the useToastState
hook. Alternatively, you could use a global toast queue (see below).
import {useToastState} from 'react-stately';
function ToastProvider({ children, ...props }) {
let state = useToastState({
maxVisibleToasts: 5
});
return (
<>
{children(state)}
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
);
}
import {useToastState} from 'react-stately';
function ToastProvider({ children, ...props }) {
let state = useToastState({
maxVisibleToasts: 5
});
return (
<>
{children(state)}
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
);
}
import {useToastState} from 'react-stately';
function ToastProvider(
{ children, ...props }
) {
let state =
useToastState({
maxVisibleToasts: 5
});
return (
<>
{children(state)}
{state
.visibleToasts
.length >
0 && (
<ToastRegion
{...props}
state={state}
/>
)}
</>
);
}
The ToastRegion
component will be rendered when there are toasts to display. It uses the useToastRegion
hook to create a landmark region, allowing keyboard and screen reader users to easily navigate to it.
import type {ToastState} from 'react-stately';
import type {AriaToastRegionProps} from 'react-aria';
import {useToastRegion} from 'react-aria';
interface ToastRegionProps<T> extends AriaToastRegionProps {
state: ToastState<T>;
}
function ToastRegion<T extends React.ReactNode>(
{ state, ...props }: ToastRegionProps<T>
) {
let ref = React.useRef(null);
let { regionProps } = useToastRegion(props, state, ref);
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => (
<Toast key={toast.key} toast={toast} state={state} />
))}
</div>
);
}
import type {ToastState} from 'react-stately';
import type {AriaToastRegionProps} from 'react-aria';
import {useToastRegion} from 'react-aria';
interface ToastRegionProps<T> extends AriaToastRegionProps {
state: ToastState<T>;
}
function ToastRegion<T extends React.ReactNode>(
{ state, ...props }: ToastRegionProps<T>
) {
let ref = React.useRef(null);
let { regionProps } = useToastRegion(props, state, ref);
return (
<div
{...regionProps}
ref={ref}
className="toast-region"
>
{state.visibleToasts.map((toast) => (
<Toast
key={toast.key}
toast={toast}
state={state}
/>
))}
</div>
);
}
import type {ToastState} from 'react-stately';
import type {AriaToastRegionProps} from 'react-aria';
import {useToastRegion} from 'react-aria';
interface ToastRegionProps<
T
> extends
AriaToastRegionProps {
state: ToastState<T>;
}
function ToastRegion<
T
extends React.ReactNode
>(
{ state, ...props }:
ToastRegionProps<T>
) {
let ref = React.useRef(
null
);
let { regionProps } =
useToastRegion(
props,
state,
ref
);
return (
<div
{...regionProps}
ref={ref}
className="toast-region"
>
{state
.visibleToasts
.map((toast) => (
<Toast
key={toast
.key}
toast={toast}
state={state}
/>
))}
</div>
);
}
Finally, we need the Toast
component to render an individual toast within a ToastRegion
, built with useToast
.
import type {AriaToastProps} from 'react-aria';
import {useToast} from 'react-aria';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
interface ToastProps<T> extends AriaToastProps<T> {
state: ToastState<T>;
}
function Toast<T extends React.ReactNode>({ state, ...props }: ToastProps<T>) {
let ref = React.useRef(null);
let { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
props,
state,
ref
);
return (
<div {...toastProps} ref={ref} className="toast">
<div {...contentProps}>
<div {...titleProps}>{props.toast.content}</div>
</div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
import type {AriaToastProps} from 'react-aria';
import {useToast} from 'react-aria';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
interface ToastProps<T> extends AriaToastProps<T> {
state: ToastState<T>;
}
function Toast<T extends React.ReactNode>(
{ state, ...props }: ToastProps<T>
) {
let ref = React.useRef(null);
let {
toastProps,
contentProps,
titleProps,
closeButtonProps
} = useToast(props, state, ref);
return (
<div {...toastProps} ref={ref} className="toast">
<div {...contentProps}>
<div {...titleProps}>{props.toast.content}</div>
</div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
import type {AriaToastProps} from 'react-aria';
import {useToast} from 'react-aria';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
interface ToastProps<T>
extends
AriaToastProps<T> {
state: ToastState<T>;
}
function Toast<
T
extends React.ReactNode
>(
{ state, ...props }:
ToastProps<T>
) {
let ref = React.useRef(
null
);
let {
toastProps,
contentProps,
titleProps,
closeButtonProps
} = useToast(
props,
state,
ref
);
return (
<div
{...toastProps}
ref={ref}
className="toast"
>
<div
{...contentProps}
>
<div
{...titleProps}
>
{props.toast
.content}
</div>
</div>
<Button
{...closeButtonProps}
>
x
</Button>
</div>
);
}
<ToastProvider>
{state => (
<Button onPress={() => state.add('Toast is done!')}>Show toast</Button>
)}
</ToastProvider>
<ToastProvider>
{(state) => (
<Button onPress={() => state.add('Toast is done!')}>
Show toast
</Button>
)}
</ToastProvider>
<ToastProvider>
{(state) => (
<Button
onPress={() =>
state.add(
'Toast is done!'
)}
>
Show toast
</Button>
)}
</ToastProvider>
Show CSS
.toast-region {
position: fixed;
bottom: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
display: flex;
align-items: center;
gap: 16px;
background: slateblue;
color: white;
padding: 12px 16px;
border-radius: 8px;
}
.toast button {
background: none;
border: none;
appearance: none;
border-radius: 50%;
height: 32px;
width: 32px;
font-size: 16px;
border: 1px solid white;
color: white;
padding: 0;
}
.toast button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white;
}
.toast button:active {
background: rgba(255, 255, 255, 0.2);
}
.toast-region {
position: fixed;
bottom: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
display: flex;
align-items: center;
gap: 16px;
background: slateblue;
color: white;
padding: 12px 16px;
border-radius: 8px;
}
.toast button {
background: none;
border: none;
appearance: none;
border-radius: 50%;
height: 32px;
width: 32px;
font-size: 16px;
border: 1px solid white;
color: white;
padding: 0;
}
.toast button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white;
}
.toast button:active {
background: rgba(255, 255, 255, 0.2);
}
.toast-region {
position: fixed;
bottom: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
display: flex;
align-items: center;
gap: 16px;
background: slateblue;
color: white;
padding: 12px 16px;
border-radius: 8px;
}
.toast button {
background: none;
border: none;
appearance: none;
border-radius: 50%;
height: 32px;
width: 32px;
font-size: 16px;
border: 1px solid white;
color: white;
padding: 0;
}
.toast button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white;
}
.toast button:active {
background: rgba(255, 255, 255, 0.2);
}
Button#
The Button
component is used in the above example to close a toast. It is built using the useButton hook, and can be shared with many other components.
Show code
import {useButton} from 'react-aria';
function Button(props) {
let ref = React.useRef(null);
let { buttonProps } = useButton(props, ref);
return <button {...buttonProps} ref={ref}>{props.children}</button>;
}
import {useButton} from 'react-aria';
function Button(props) {
let ref = React.useRef(null);
let { buttonProps } = useButton(props, ref);
return (
<button {...buttonProps} ref={ref}>
{props.children}
</button>
);
}
import {useButton} from 'react-aria';
function Button(props) {
let ref = React.useRef(
null
);
let { buttonProps } =
useButton(
props,
ref
);
return (
<button
{...buttonProps}
ref={ref}
>
{props.children}
</button>
);
}
Usage#
The following examples show how to use the ToastProvider
component created in the above example.
Toast priorities#
Toasts are displayed according to a priority queue. The priority of a toast can be set using the priority
option, passed to the state.add
function. Priorities are arbitrary numbers defined by your implementation.
<ToastProvider>
{state => (<>
<Button onPress={() => state.add('Toasting…', {priority: 1})}> Show low priority toast
</Button>
<Button onPress={() => state.add('Toast is done!', {priority: 2})}> Show medium priority toast
</Button>
<Button onPress={() => state.add('Toast is burned!', {priority: 3})}> Show high priority toast
</Button>
</>)}
</ToastProvider>
<ToastProvider>
{(state) => (
<>
<Button
onPress={() =>
state.add('Toasting…', { priority: 1 })}
> Show low priority toast
</Button>
<Button
onPress={() =>
state.add('Toast is done!', { priority: 2 })}
> Show medium priority toast
</Button>
<Button
onPress={() =>
state.add('Toast is burned!', { priority: 3 })}
> Show high priority toast
</Button>
</>
)}
</ToastProvider>
<ToastProvider>
{(state) => (
<>
<Button
onPress={() =>
state.add(
'Toasting…',
{
priority:
1
}
)}
> Show low
priority toast
</Button>
<Button
onPress={() =>
state.add(
'Toast is done!',
{
priority:
2
}
)}
> Show medium
priority toast
</Button>
<Button
onPress={() =>
state.add(
'Toast is burned!',
{
priority:
3
}
)}
> Show high
priority toast
</Button>
</>
)}
</ToastProvider>
Auto-dismiss#
Toasts support a timeout
option to automatically hide them after a certain amount of time. For accessibility, toasts should have a minimum timeout of 5 seconds to give users enough time to read them. If a toast includes action buttons or other interactive elements it should not auto dismiss. In addition, timers will automatically pause when the user focuses or hovers over a toast.
Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely.
<ToastProvider>
{state => (
<Button onPress={() => state.add('Toast is done!', {timeout: 5000})}> Show toast
</Button>
)}
</ToastProvider>
<ToastProvider>
{(state) => (
<Button
onPress={() =>
state.add('Toast is done!', { timeout: 5000 })}
> Show toast
</Button>
)}
</ToastProvider>
<ToastProvider>
{(state) => (
<Button
onPress={() =>
state.add(
'Toast is done!',
{
timeout:
5000
}
)}
> Show toast
</Button>
)}
</ToastProvider>
Programmatic dismissal#
Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. state.add
returns a key for the toast which may be passed to state.close
to dismiss the toast.
function Example() {
let [toastKey, setToastKey] = React.useState(null);
return (
<ToastProvider>
{(state) => (
<Button
onPress={() => {
if (!toastKey) {
setToastKey(
state.add('Unable to save', {
onClose: () => setToastKey(null)
})
); } else {
state.close(toastKey); }
}}
>
{toastKey ? 'Hide' : 'Show'} Toast
</Button>
)}
</ToastProvider>
);
}
function Example() {
let [toastKey, setToastKey] = React.useState(null);
return (
<ToastProvider>
{(state) => (
<Button
onPress={() => {
if (!toastKey) {
setToastKey(
state.add('Unable to save', {
onClose: () => setToastKey(null)
})
); } else {
state.close(toastKey); }
}}
>
{toastKey ? 'Hide' : 'Show'} Toast
</Button>
)}
</ToastProvider>
);
}
function Example() {
let [
toastKey,
setToastKey
] = React.useState(
null
);
return (
<ToastProvider>
{(state) => (
<Button
onPress={() => {
if (
!toastKey
) {
setToastKey(
state
.add(
'Unable to save',
{
onClose:
() =>
setToastKey(
null
)
}
)
); } else {
state
.close(
toastKey
); }
}}
>
{toastKey
? 'Hide'
: 'Show'}
{' '}
Toast
</Button>
)}
</ToastProvider>
);
}
Advanced topics#
Global toast queue#
In the above examples, each ToastProvider
has a separate queue. This setup is simple, and fine for most cases where you can wrap the entire app in a single ToastProvider
. However, in more complex situations, you may want to keep the toast queue outside the React tree so that toasts can be queued from anywhere. This can be done by creating your own ToastQueue
and subscribing to it using the useToastQueue
hook rather than useToastState
.
import {ToastQueue, useToastQueue} from 'react-stately';
import {createPortal} from 'react-dom';
// Create a global toast queue.
const toastQueue = new ToastQueue({
maxVisibleToasts: 5
});
function GlobalToastRegion(props) {
// Subscribe to it.
let state = useToastQueue(toastQueue);
// Render toast region.
return state.visibleToasts.length > 0
? createPortal(<ToastRegion {...props} state={state} />, document.body)
: null;
}
// Render it somewhere in your app.
<GlobalToastRegion />
import {ToastQueue, useToastQueue} from 'react-stately';
import {createPortal} from 'react-dom';
// Create a global toast queue.
const toastQueue = new ToastQueue({
maxVisibleToasts: 5
});
function GlobalToastRegion(props) {
// Subscribe to it.
let state = useToastQueue(toastQueue);
// Render toast region.
return state.visibleToasts.length > 0
? createPortal(
<ToastRegion {...props} state={state} />,
document.body
)
: null;
}
// Render it somewhere in your app.
<GlobalToastRegion />
import {
ToastQueue,
useToastQueue
} from 'react-stately';
import {createPortal} from 'react-dom';
// Create a global toast queue.
const toastQueue =
new ToastQueue({
maxVisibleToasts: 5
});
function GlobalToastRegion(
props
) {
// Subscribe to it.
let state =
useToastQueue(
toastQueue
);
// Render toast region.
return state
.visibleToasts
.length > 0
? createPortal(
<ToastRegion
{...props}
state={state}
/>,
document.body
)
: null;
}
// Render it somewhere in your app.
<GlobalToastRegion />
Now you can queue a toast from anywhere:
<Button onPress={() => toastQueue.add('Toast is done!')}>Show toast</Button>
<Button onPress={() => toastQueue.add('Toast is done!')}>
Show toast
</Button>
<Button
onPress={() =>
toastQueue.add(
'Toast is done!'
)}
>
Show toast
</Button>
Animations#
useToastState
and ToastQueue
support a hasExitAnimation
option. When enabled, toasts transition to an "exiting" state when closed rather than immediately being removed. This allows you to trigger an exit animation. When complete, call the state.remove
function.
Each QueuedToast
includes an animation
property that indicates the current animation state. There are three possible states:
entering
– The toast is entering immediately after being triggered.queued
– The toast is entering from the queue (out of view).exiting
– The toast is exiting from view.
function ToastRegion() {
let state = useToastState({
maxVisibleToasts: 5,
hasExitAnimation: true });
// ...
}
function Toast({state, ...props}) {
let ref = React.useRef(null);
let {toastProps, titleProps, closeButtonProps} = useToast(props, state, ref);
return (
<div
{...toastProps}
ref={ref}
className="toast"
// Use a data attribute to trigger animations in CSS.
data-animation={props.toast.animation}
onAnimationEnd={() => {
// Remove the toast when the exiting animation completes.
if (props.toast.animation === 'exiting') {
state.remove(props.toast.key);
}
}} >
<div {...titleProps}>{props.toast.content}</div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
function ToastRegion() {
let state = useToastState({
maxVisibleToasts: 5,
hasExitAnimation: true });
// ...
}
function Toast({ state, ...props }) {
let ref = React.useRef(null);
let { toastProps, titleProps, closeButtonProps } =
useToast(props, state, ref);
return (
<div
{...toastProps}
ref={ref}
className="toast"
// Use a data attribute to trigger animations in CSS.
data-animation={props.toast.animation}
onAnimationEnd={() => {
// Remove the toast when the exiting animation completes.
if (props.toast.animation === 'exiting') {
state.remove(props.toast.key);
}
}} >
<div {...titleProps}>{props.toast.content}</div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
function ToastRegion() {
let state =
useToastState({
maxVisibleToasts:
5,
hasExitAnimation:
true });
// ...
}
function Toast(
{ state, ...props }
) {
let ref = React.useRef(
null
);
let {
toastProps,
titleProps,
closeButtonProps
} = useToast(
props,
state,
ref
);
return (
<div
{...toastProps}
ref={ref}
className="toast"
// Use a data attribute to trigger animations in CSS.
data-animation={props
.toast.animation}
onAnimationEnd={() => {
// Remove the toast when the exiting animation completes.
if (
props.toast
.animation ===
'exiting'
) {
state.remove(
props.toast
.key
);
}
}} >
<div
{...titleProps}
>
{props.toast
.content}
</div>
<Button
{...closeButtonProps}
>
x
</Button>
</div>
);
}
In CSS, the data attribute defined above can be used to trigger keyframe animations:
.toast[data-animation=entering] {
animation-name: slide-in;
}
.toast[data-animation=queued] {
animation-name: fade-in;
}
.toast[data-animation=exiting] {
animation-name: slide-out;
}
.toast[data-animation=entering] {
animation-name: slide-in;
}
.toast[data-animation=queued] {
animation-name: fade-in;
}
.toast[data-animation=exiting] {
animation-name: slide-out;
}
.toast[data-animation=entering] {
animation-name: slide-in;
}
.toast[data-animation=queued] {
animation-name: fade-in;
}
.toast[data-animation=exiting] {
animation-name: slide-out;
}
TypeScript#
A ToastQueue
and useToastState
use a generic type to represent toast content. The examples so far have used strings, but you can type this however you want to enable passing custom objects or options. This example uses a custom object to support toasts with both a title and description.
import type {QueuedToast} from 'react-stately';
interface MyToast {
title: string;
description: string;
}
function ToastProvider() {
let state = useToastState<MyToast>();
// ...
}
interface ToastProps {
toast: QueuedToast<MyToast>;}
function Toast(props: ToastProps) {
// ...
let { toastProps, titleProps, descriptionProps, closeButtonProps } = useToast(
props,
state,
ref
);
return (
<div {...toastProps} ref={ref} className="toast">
<div>
<div {...titleProps}>{props.toast.content.title}</div>
<div {...descriptionProps}>{props.toast.content.description}</div> </div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
// Queuing a toast
state.add({ title: 'Success!', description: 'Toast is done.' });
import type {QueuedToast} from 'react-stately';
interface MyToast {
title: string;
description: string;
}
function ToastProvider() {
let state = useToastState<MyToast>();
// ...
}
interface ToastProps {
toast: QueuedToast<MyToast>;}
function Toast(props: ToastProps) {
// ...
let {
toastProps,
titleProps,
descriptionProps,
closeButtonProps
} = useToast(props, state, ref);
return (
<div {...toastProps} ref={ref} className="toast">
<div>
<div {...titleProps}>
{props.toast.content.title}
</div>
<div {...descriptionProps}>
{props.toast.content.description}
</div> </div>
<Button {...closeButtonProps}>x</Button>
</div>
);
}
// Queuing a toast
state.add({
title: 'Success!',
description: 'Toast is done.'
});
import type {QueuedToast} from 'react-stately';
interface MyToast {
title: string;
description: string;
}
function ToastProvider() {
let state =
useToastState<
MyToast
>();
// ...
}
interface ToastProps {
toast: QueuedToast<
MyToast
>;}
function Toast(
props: ToastProps
) {
// ...
let {
toastProps,
titleProps,
descriptionProps,
closeButtonProps
} = useToast(
props,
state,
ref
);
return (
<div
{...toastProps}
ref={ref}
className="toast"
>
<div>
<div
{...titleProps}
>
{props.toast
.content
.title}
</div>
<div
{...descriptionProps}
>
{props.toast
.content
.description}
</div> </div>
<Button
{...closeButtonProps}
>
x
</Button>
</div>
);
}
// Queuing a toast
state.add({
title: 'Success!',
description:
'Toast is done.'
});