beta

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.

installyarn add @react-aria/toast
version3.0.0-beta.5
usageimport {useToastRegion, useToast} from '@react-aria/toast'

API#


useToastRegion<T>( props: AriaToastRegionProps, state: ToastState<T>, ref: RefObject<HTMLElement> ): ToastRegionAria useToast<T>( props: AriaToastProps<T>, state: ToastState<T>, ref: RefObject<FocusableElement> ): 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#


Analysis completeTitleClose buttonToastRegion

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:

NameTypeDescription
regionPropsDOMAttributesProps for the landmark region element.

useToast returns props that you should spread onto an individual toast and its child elements:

NameTypeDescription
toastPropsDOMAttributesProps for the toast container element.
titlePropsDOMAttributesProps for the toast title element.
descriptionPropsDOMAttributesProps for the toast description element, if any.
closeButtonPropsAriaButtonPropsProps 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/toast';

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/toast';

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/toast';

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 {AriaToastRegionProps} from '@react-aria/toast';
import type {ToastState} from '@react-stately/toast';
import {useToastRegion} from '@react-aria/toast';

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 {AriaToastRegionProps} from '@react-aria/toast';
import type {ToastState} from '@react-stately/toast';
import {useToastRegion} from '@react-aria/toast';

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 {AriaToastRegionProps} from '@react-aria/toast';
import type {ToastState} from '@react-stately/toast';
import {useToastRegion} from '@react-aria/toast';

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/toast';
import {useToast} from '@react-aria/toast';

// 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, titleProps, closeButtonProps} = useToast(props, state, ref);

  return (
    <div {...toastProps} ref={ref} className="toast">
      <div {...titleProps}>{props.toast.content}</div>
      <Button {...closeButtonProps}>x</Button>
    </div>
  );
}
import type {AriaToastProps} from '@react-aria/toast';
import {useToast} from '@react-aria/toast';

// 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, titleProps, closeButtonProps } =
    useToast(props, state, ref);

  return (
    <div {...toastProps} ref={ref} className="toast">
      <div {...titleProps}>{props.toast.content}</div>
      <Button {...closeButtonProps}>x</Button>
    </div>
  );
}
import type {AriaToastProps} from '@react-aria/toast';
import {useToast} from '@react-aria/toast';

// 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,
    titleProps,
    closeButtonProps
  } = useToast(
    props,
    state,
    ref
  );

  return (
    <div
      {...toastProps}
      ref={ref}
      className="toast"
    >
      <div
        {...titleProps}
      >
        {props.toast
          .content}
      </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/button';

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/button';

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/button';

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/toast';

// 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
    ? ReactDOM.createPortal(
      <ToastRegion {...props} state={state} />,
      document.body
    )
    : null;
}

// Render it somewhere in your app.
<GlobalToastRegion />
import {
  ToastQueue,
  useToastQueue
} from '@react-stately/toast';

// 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
    ? ReactDOM.createPortal(
      <ToastRegion {...props} state={state} />,
      document.body
    )
    : null;
}

// Render it somewhere in your app.
<GlobalToastRegion />
import {
  ToastQueue,
  useToastQueue
} from '@react-stately/toast';

// 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
    ? ReactDOM
      .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/toast';

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/toast';

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/toast';

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.'
});