Toast

A succinct message that is displayed temporarily.

import * as React from 'react';
import { styled, keyframes } from '@stitches/react';
import { violet, blackA, mauve, slate, green } from '@radix-ui/colors';
import * as ToastPrimitive from '@radix-ui/react-toast';
const VIEWPORT_PADDING = 25;
const hide = keyframes({
'0%': { opacity: 1 },
'100%': { opacity: 0 },
});
const slideIn = keyframes({
from: { transform: `translateX(calc(100% + ${VIEWPORT_PADDING}px))` },
to: { transform: 'translateX(0)' },
});
const swipeOut = keyframes({
from: { transform: 'translateX(var(--radix-toast-swipe-end-x))' },
to: { transform: `translateX(calc(100% + ${VIEWPORT_PADDING}px))` },
});
const StyledViewport = styled(ToastPrimitive.Viewport, {
position: 'fixed',
bottom: 0,
right: 0,
display: 'flex',
flexDirection: 'column',
padding: VIEWPORT_PADDING,
gap: 10,
width: 390,
maxWidth: '100vw',
margin: 0,
listStyle: 'none',
zIndex: 2147483647,
outline: 'none',
});
const StyledToast = styled(ToastPrimitive.Root, {
backgroundColor: 'white',
borderRadius: 6,
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
padding: 15,
display: 'grid',
gridTemplateAreas: '"title action" "description action"',
gridTemplateColumns: 'auto max-content',
columnGap: 15,
alignItems: 'center',
'@media (prefers-reduced-motion: no-preference)': {
'&[data-state="open"]': {
animation: `${slideIn} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
'&[data-state="closed"]': {
animation: `${hide} 100ms ease-in forwards`,
},
'&[data-swipe="move"]': {
transform: 'translateX(var(--radix-toast-swipe-move-x))',
},
'&[data-swipe="cancel"]': {
transform: 'translateX(0)',
transition: 'transform 200ms ease-out',
},
'&[data-swipe="end"]': {
animation: `${swipeOut} 100ms ease-out forwards`,
},
},
});
const StyledTitle = styled(ToastPrimitive.Title, {
gridArea: 'title',
marginBottom: 5,
fontWeight: 500,
color: slate.slate12,
fontSize: 15,
});
const StyledDescription = styled(ToastPrimitive.Description, {
gridArea: 'description',
margin: 0,
color: slate.slate11,
fontSize: 13,
lineHeight: 1.3,
});
const StyledAction = styled(ToastPrimitive.Action, {
gridArea: 'action',
});
// Exports
export const ToastProvider = ToastPrimitive.Provider;
export const ToastViewport = StyledViewport;
export const Toast = StyledToast;
export const ToastTitle = StyledTitle;
export const ToastDescription = StyledDescription;
export const ToastAction = StyledAction;
export const ToastClose = ToastPrimitive.Close;
// Your app...
const Box = styled('div', {});
const Button = styled('button', {
all: 'unset',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
padding: '0 15px',
fontSize: 15,
lineHeight: 1,
fontWeight: 500,
height: 35,
variants: {
size: {
small: {
fontSize: 12,
padding: '0 10px',
lineHeight: '25px',
height: 25,
},
},
variant: {
violet: {
backgroundColor: 'white',
color: violet.violet11,
boxShadow: `0 2px 10px ${blackA.blackA7}`,
'&:hover': { backgroundColor: mauve.mauve3 },
'&:focus': { boxShadow: `0 0 0 2px black` },
},
green: {
backgroundColor: green.green2,
color: green.green11,
boxShadow: `inset 0 0 0 1px ${green.green7}`,
'&:hover': { boxShadow: `inset 0 0 0 1px ${green.green8}` },
'&:focus': { boxShadow: `0 0 0 2px ${green.green8}` },
},
},
},
defaultVariants: {
variant: 'violet',
},
});
const ToastDemo = () => {
const [open, setOpen] = React.useState(false);
const eventDateRef = React.useRef(new Date());
const timerRef = React.useRef(0);
React.useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
return (
<ToastProvider swipeDirection="right">
<Button onClick={() => { setOpen(false); window.clearTimeout(timerRef.current); timerRef.current = window.setTimeout(() => { eventDateRef.current = oneWeekAway(); setOpen(true); }, 100); }} >
Add to calendar
</Button>
<Toast open={open} onOpenChange={setOpen}>
<ToastTitle>Scheduled: Catch up</ToastTitle>
<ToastDescription asChild>
<time dateTime={eventDateRef.current.toISOString()}>
{prettyDate(eventDateRef.current)}
</time>
</ToastDescription>
<ToastAction asChild altText="Goto schedule to undo">
<Button variant="green" size="small">
Undo
</Button>
</ToastAction>
</Toast>
<ToastViewport />
</ToastProvider>
);
};
function oneWeekAway(date) {
const now = new Date();
const inOneWeek = now.setDate(now.getDate() + 7);
return new Date(inOneWeek);
}
function prettyDate(date) {
return new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'short' }).format(date);
}
export default ToastDemo;

Features

  • Automatically closes.
  • Pauses closing on hover, focus and window blur.
  • Supports hotkey to jump to toast viewport.
  • Supports closing via swipe gesture.
  • Exposes CSS variables for swipe gesture animations.
  • Can be controlled or uncontrolled.

Install the component from your command line.

npm install @radix-ui/react-toast

Import the component.

import * as Toast from '@radix-ui/react-toast';
export default () => (
<Toast.Provider>
<Toast.Root>
<Toast.Title />
<Toast.Description />
<Toast.Action />
<Toast.Close />
</Toast.Root>
<Toast.Viewport />
</Toast.Provider>
);

The provider that wraps your toasts and toast viewport. It usually wraps the application.

PropTypeDefault
durationnumber5000
swipeDirectionenum"right"
swipeThresholdnumber50

The fixed area where toasts appear. Users can jump to the viewport by pressing a hotkey. It is up to you to ensure the discoverability of the hotkey for keyboard users.

PropTypeDefault
asChildbooleanfalse
hotkeystring[]["F8"]
labelstring"Notifications ({hotkey})"

The toast that automatically closes. It should not be held open to acquire a user response.

PropTypeDefault
asChildbooleanfalse
typeenum"foreground"
durationnumberNo default value
defaultOpenbooleantrue
openbooleanNo default value
onOpenChangefunctionNo default value
forceMountbooleanNo default value

An optional title for the toast.

PropTypeDefault
asChildbooleanfalse

The toast message.

PropTypeDefault
asChildbooleanfalse

An action that is safe to ignore to ensure users are not expected to complete tasks with unexpected side effects as a result of a time limit.

When obtaining a user response is necessary, portal an AlertDialog styled as a toast into the viewport instead.

PropTypeDefault
asChildbooleanfalse
altText*stringNo default value

A button that allows users to dismiss the toast before its duration has elapsed.

PropTypeDefault
asChildbooleanfalse

Override the default hotkey using the event.code value for each key from keycode.info.

<Toast.Provider>
{/* ... */}
<Toast.Viewport hotkey={['altKey', 'KeyT']} />
</Toast.Provider>

Customise the duration of a toast to override the provider value.

<Toast.Root duration={3000}>
<Toast.Description>Saved!</Toast.Description>
</Toast.Root>

When a toast must appear every time a user clicks a button, use state to render multiple instances of the same toast (see below). Alternatively, you can abstract the parts to create your own imperative API.

export default () => {
const [savedCount, setSavedCount] = React.useState(0);
return (
<div>
<form onSubmit={() => setSavedCount((count) => count + 1)}>
{/* ... */}
<button>save</button>
</form>
{Array.from({ length: savedCount }).map((_, index) => (
<Toast.Root key={index}>
<Toast.Description>Saved!</Toast.Description>
</Toast.Root>
))}
</div>
);
};

Combine --radix-toast-swipe-move-[x|y] and --radix-toast-swipe-end-[x|y] CSS variables with data-swipe="[start|move|cancel|end]" attributes to animate a swipe to close gesture. Here's an example using Stitches:

import { styled, keyframes } from '@stitches/react';
import * as ToastPrimitive from '@radix-ui/react-toast';
const slideRight = css.keyframes({
from: { transform: 'translateX(var(--radix-toast-swipe-end-x))' },
to: { transform: 'translateX(100%)' },
});
const Toast = styled(ToastPrimitive.Root, {
'@media (prefers-reduced-motion: no-preference)': {
'&[data-swipe="move"]': {
transform: 'translateX(var(--radix-toast-swipe-move-x))',
},
'&[data-swipe="cancel"]': {
transform: 'translateX(0)',
transition: 'transform 200ms ease-out',
},
'&[data-swipe="end"]': {
animation: `${slideRight} 100ms ease-out forwards`,
},
},
});
export default () => (
<ToastPrimitive.Provider swipeDirection="right">
<Toast>...</Toast>
<ToastPrimitive.Viewport />
</ToastPrimitive.Provider>
);

Adheres to the aria-live requirements.

Control the sensitivity of the toast for screen readers using the type prop.

For toasts that are the result of a user action, choose foreground. Toasts generated from background tasks should use background.

Foreground

Foreground toasts are announced immediately. Assistive technologies may choose to clear previously queued messages when a foreground toast appears. Try to avoid stacking distinct foreground toasts at the same time.

Background

Background toasts are announced at the next graceful opportunity, for example, when the screen reader has finished reading its current sentence. They do not clear queued messages so overusing them can be perceived as a laggy user experience for screen reader users when used in response to a user interaction.

<Toast.Root type="foreground">
<Toast.Description>File removed successfully.</Toast.Description>
<Toast.Close>Dismiss</Toast.Close>
</Toast.Root>
<Toast.Root type="background">
<Toast.Description>We've just released Radix 1.0.</Toast.Description>
<Toast.Close>Dismiss</Toast.Close>
</Toast.Root>

Use the altText prop on the Action to instruct an alternative way of actioning the toast to screen reader users.

You can direct the user to a permanent place in your application where they can action it or implement your own custom hotkey logic. If implementing the latter, use foreground type to announce immediately and increase the duration to give the user ample time.

<Toast.Root type="background">
<Toast.Title>Upgrade Available!</Toast.Title>
<Toast.Description>We've just released Radix 1.0.</Toast.Description>
<Toast.Action altText="Goto account settings to upgrade">
Upgrade
</Toast.Action>
<Toast.Close>Dismiss</Toast.Close>
</Toast.Root>
<Toast.Root type="foreground" duration={10000}>
<Toast.Description>File removed successfully.</Toast.Description>
<Toast.Action altText="Undo (Alt+U)">
Undo <kbd>Alt</kbd>+<kbd>U</kbd>
</Toast.Action>
<Toast.Close>Dismiss</Toast.Close>
</Toast.Root>

When providing an icon (or font icon), remember to label it correctly for screen reader users.

<Toast.Root type="foreground">
<Toast.Description>Saved!</Toast.Description>
<Toast.Close aria-label="Close">
<span aria-hidden>×</span>
</Toast.Close>
</Toast.Root>
KeyDescription
F8Focuses toasts viewport.
TabMoves focus to the next focusable element.
Shift + TabMoves focus to the previous focusable element.
SpaceWhen focus is on a Toast.Action or Toast.Close, closes the toast.
EnterWhen focus is on a Toast.Action or Toast.Close, closes the toast.
EscWhen focus is on a Toast, closes the toast.

Create your own API by abstracting the primitive parts into your own component.

Usage

import { Toast } from './your-toast';
export default () => (
<Toast title="Upgrade available" content="We've just released Radix 3.0!">
<button onClick={handleUpgrade}>Upgrade</button>
</Toast>
);

Implementation

// your-toast.jsx
import * as ToastPrimitive from '@radix-ui/react-toast';
export const Toast = ({ title, content, children, ...props }) => {
return (
<ToastPrimitive.Root {...props}>
{title && <ToastPrimitive.Title>{title}</ToastPrimitive.Title>}
<ToastPrimitive.Description>{content}</ToastPrimitive.Description>
{children && (
<ToastPrimitive.Action asChild>{children}</ToastPrimitive.Action>
)}
<ToastPrimitive.Close aria-label="Close">
<span aria-hidden>×</span>
</ToastPrimitive.Close>
</ToastPrimitive.Root>
);
};

Create your own imperative API to allow toast duplication if preferred.

Usage

import { Toast } from './your-toast';
export default () => {
const savedRef = React.useRef();
return (
<div>
<form onSubmit={() => savedRef.current.publish()}>
{/* ... */}
<button>Save<button>
</form>
<Toast ref={savedRef}>Saved successfully!</Toast>
</div>
);
};

Implementation

// your-toast.jsx
import React from 'react';
import * as ToastPrimitive from '@radix-ui/react-toast';
export const Toast = React.forwardRef((props, forwardedRef) => {
const { children, ...toastProps } = props;
const [count, setCount] = React.useState(0);
React.useImperativeHandle(forwardedRef, () => ({
publish: () => setCount((count) => count + 1),
}));
return (
<>
{Array.from({ length: count }).map((_, index) => (
<ToastPrimitive.Root key={index} {...toastProps}>
<ToastPrimitive.Description>{children}</ToastPrimitive.Description>
<ToastPrimitive.Close>Dismiss</ToastPrimitive.Close>
</ToastPrimitive.Root>
))}
</>
);
});