Toast
- 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.
- Add, update and close toasts from anywhere with a toast manager.
- Supports stacking, toast limits and anchoring toasts to an element.
Installation
Install the component from your command line.
$ npm add reka-uiAnatomy
Import the component.
<script setup lang="ts">
import { ToastAction, ToastClose, ToastDescription, ToastProvider, ToastRoot, ToastTitle, ToastViewport } from 'reka-ui'
</script>
<template>
<ToastProvider>
<ToastRoot>
<ToastTitle />
<ToastDescription />
<ToastAction />
<ToastClose />
</ToastRoot>
<ToastViewport />
</ToastProvider>
</template>API Reference
Provider
The provider that wraps your toasts and toast viewport. It usually wraps the application.
Viewport
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.
| Data Attribute | Value |
|---|---|
[data-expanded] | Present while the pointer is over the viewport or focus is inside it. |
| CSS Variable | Description |
|---|---|
--reka-toast-frontmost-height | The natural height of the newest open toast, in pixels. |
Root
The toast that automatically closes. It should not be held open to acquire a user response.
| Data Attribute | Value |
|---|---|
[data-state] | "open" | "closed" |
[data-swipe] | "start" | "move" | "cancel" | "end" |
[data-swipe-direction] | "up" | "down" | "left" | "right" |
[data-status] | The status of a toast from the toast manager, e.g. loading, success or error. |
[data-expanded] | Present while the viewport is expanded. |
[data-limited] | Present when the toast exceeds the limit of ToastProvider. The toast is also inert. |
| CSS Variable | Description |
|---|---|
--reka-toast-swipe-move-x | The offset position of the toast when horizontally swiping |
--reka-toast-swipe-move-y | The offset position of the toast when vertically swiping |
--reka-toast-swipe-end-x | The offset end position of the toast after horizontally swiping |
--reka-toast-swipe-end-y | The offset end position of the toast after vertically swiping |
--reka-toast-index | The position of the toast in the stack. The newest open toast is 0. |
--reka-toast-offset-y | The sum of the heights of the newer open toasts, in pixels. Use it to lay out an expanded stack. |
--reka-toast-height | The natural height of the toast, in pixels. |
Portal
When used, portals the content part into the body.
Title
An optional title for the toast
Description
The toast message.
Action
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.
By default, clicking ToastAction dismisses the toast. Set closeOnClick to false when the action should run while keeping the toast visible.
<template>
<ToastAction
alt-text="Retry sending the message"
:close-on-click="false"
@click="retry"
>
Retry
</ToastAction>
</template>Close
A button that allows users to dismiss the toast before its duration has elapsed.
Positioner
Positions a toast against an anchor element. Wrap ToastRoot in it to show a toast next to the element it relates to. Any prop that is not set falls back to toast.positionerProps.
| Data Attribute | Value |
|---|---|
[data-side] | "left" | "right" | "bottom" | "top" |
[data-align] | "start" | "end" | "center" |
Arrow
An optional arrow element to render alongside an anchored toast. Must be rendered inside ToastPositioner.
useToastManager
Returns the toasts of the nearest ToastProvider, newest first, and the methods to manage them. It must be called inside ToastProvider.
| Prop | Default | Type |
|---|---|---|
toasts | Ref<ToastObject[]>The toasts, newest first. Closed toasts stay in the list until their exit animation ends. | |
add | (options: ToastAddOptions) => stringAdds a toast and returns its id. Adding a toast with the id of an open toast updates it and restarts its timer. | |
update | (id: string, updates: ToastUpdateOptions | ((prev: ToastObject) => ToastUpdateOptions)) => voidUpdates an open toast with new options, or with a function of the previous toast. | |
close | (id?: string) => voidCloses the toast with the given id, or every toast when no id is given. | |
promise | (promise: Promise<T>, options: ToastPromiseOptions<T>) => Promise<T>Shows a loading toast that turns into a success or error toast when the promise settles. Returns the promise. |
The options of a toast:
| Prop | Default | Type |
|---|---|---|
id | stringThe unique identifier. Generated when not given. | |
title | stringRendered by ToastTitle when it has no slot content. | |
description | stringRendered by ToastDescription when it has no slot content. | |
type | 'foreground' | 'background'The sensitivity for accessibility purposes. Overrides the type prop of ToastRoot. | |
status | 'loading' | 'success' | 'error' | stringExposed as data-status. loading toasts are never dismissed automatically. | |
duration | numberTime in milliseconds before the toast closes. 0 or Infinity keeps it open. Overrides the duration of ToastRoot and ToastProvider. | |
actionProps | { label?: string, altText: string, closeOnClick?: boolean, onClick?: (event: MouseEvent) => void }Renders ToastAction with this label. altText is required; closeOnClick defaults to true. | |
positionerProps | ToastPositionerOptionsProps for ToastPositioner, including the anchor element. | |
data | DataCustom data for your own rendering. | |
onClose | () => voidCalled when the toast closes. | |
onRemove | () => voidCalled when the toast is removed from the list, after its exit animation. |
Each toast in toasts also has open and an updateKey that increments whenever it is updated or added again.
createToastManager
Creates a manager that adds toasts from anywhere, including outside components, such as in API clients or stores. Pass it to the toastManager prop of ToastProvider. It has the same add, update, close and promise methods as useToastManager, but not the toasts list.
Toasts added on the client before a ToastProvider has mounted are shown once it mounts. Toasts added during server-side rendering are dropped.
Examples
Toast manager
Render the toasts of useToastManager() in one place, and add toasts with it from any component inside ToastProvider. Pass each toast to ToastRoot; ToastTitle, ToastDescription and ToastAction render the toast's content when they have no slot content.
<!-- Toaster.vue -->
<script setup lang="ts">
import { ToastAction, ToastClose, ToastDescription, ToastRoot, ToastTitle, ToastViewport, useToastManager } from 'reka-ui'
const { toasts } = useToastManager()
</script>
<template>
<ToastRoot
v-for="toast in toasts"
:key="toast.id"
:toast="toast"
>
<ToastTitle />
<ToastDescription />
<ToastAction />
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
<ToastViewport />
</template><!-- App.vue -->
<template>
<ToastProvider>
<SaveButton />
<Toaster />
</ToastProvider>
</template><!-- SaveButton.vue -->
<script setup lang="ts">
import { useToastManager } from 'reka-ui'
const toast = useToastManager()
function save() {
toast.add({
title: 'Saved',
description: 'Your changes have been saved.',
actionProps: { label: 'Undo', altText: 'Undo save', onClick: undo },
})
}
</script>A closed toast is removed from toasts once its exit animation ends.
Global manager
Create a manager with createToastManager() to add toasts from outside components. Because it holds no state itself, it is safe to create at module level, including with SSR.
// toast.ts
import { createToastManager } from 'reka-ui'
export const toast = createToastManager()<!-- App.vue -->
<script setup lang="ts">
import { toast } from './toast'
</script>
<template>
<ToastProvider :toast-manager="toast">
<RouterView />
<Toaster />
</ToastProvider>
</template>// api.ts
import { toast } from './toast'
export async function fetchJson(url: string) {
const response = await fetch(url)
if (!response.ok)
toast.add({ title: 'Request failed', description: response.statusText })
return response.json()
}Promise
promise() shows a toast with status: 'loading', which does not close by itself, then updates it to success or error when the promise settles. Style each state with data-status.
toast.promise(saveDocument(), {
loading: 'Saving…',
success: doc => `Saved ${doc.name}`,
error: error => ({ title: 'Could not save', description: error.message }),
})Updating and deduplicating toasts
Keep the id returned by add() to update the toast later. Adding a toast with the id of a toast that is still open updates it in place and restarts its timer, instead of showing a duplicate.
const id = toast.add({ title: 'Uploading…', duration: 0 })
toast.update(id, prev => ({ title: 'Uploaded', duration: 3000 }))
// Only one "Offline" toast, however often this runs.
toast.add({ id: 'offline', title: 'You are offline' })Stacking toasts
Set limit on ToastProvider to cap the number of toasts shown. Older toasts beyond the limit get data-limited and inert so you can hide them. Each toast exposes --reka-toast-index, --reka-toast-offset-y and --reka-toast-height, and the viewport exposes --reka-toast-frontmost-height and data-expanded, to build a stack that expands on hover or focus.
<template>
<ToastProvider :limit="3">
<Toaster />
</ToastProvider>
</template>.ToastRoot {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
transition: transform 300ms, opacity 300ms, height 300ms;
/* Collapsed: each toast peeks out behind the newer one */
height: var(--reka-toast-frontmost-height);
transform: translateY(calc(var(--reka-toast-index) * -12px)) scale(calc(1 - var(--reka-toast-index) * 0.05));
z-index: calc(1000 - var(--reka-toast-index));
}
.ToastViewport[data-expanded] .ToastRoot {
height: var(--reka-toast-height);
transform: translateY(calc(var(--reka-toast-offset-y) * -1 - var(--reka-toast-index) * 12px));
}
.ToastRoot[data-limited] {
opacity: 0;
}Anchored toasts
Wrap ToastRoot in ToastPositioner to show a toast next to an element, such as a "Copied" toast by a copy button. Pass the anchor element with positionerProps when adding the toast. Use a separate ToastProvider for anchored toasts, so they do not stack with the others.
<!-- AnchoredToaster.vue -->
<script setup lang="ts">
import { ToastArrow, ToastPositioner, ToastRoot, ToastTitle, ToastViewport, useToastManager } from 'reka-ui'
const { toasts } = useToastManager()
</script>
<template>
<ToastViewport as="div">
<ToastPositioner
v-for="toast in toasts"
:key="toast.id"
:toast="toast"
:side-offset="8"
>
<ToastRoot
:toast="toast"
as="div"
>
<ToastTitle />
<ToastArrow />
</ToastRoot>
</ToastPositioner>
</ToastViewport>
</template>function copy(event: MouseEvent) {
navigator.clipboard.writeText(text)
anchoredToast.add({
title: 'Copied',
duration: 1500,
positionerProps: { anchor: event.currentTarget as HTMLElement },
})
}Custom hotkey
Override the default hotkey using the event.code value for each key from keycode.info.
<template>
<ToastProvider>
...
<ToastViewport :hotkey="['altKey', 'KeyT']" />
</ToastProvider>
</template>Custom duration
Customise the duration of a toast to override the provider value.
<template>
<ToastRoot :duration="3000">
<ToastDescription>Saved!</ToastDescription>
</ToastRoot>
</template>Duplicate toasts
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.
<template>
<div>
<form @submit="count++">
...
<button>save</button>
</form>
<ToastRoot v-for="(_, index) in count" :key="index">
<ToastDescription>Saved!</ToastDescription>
</ToastRoot>
</div>
</template>Animating swipe gesture
Combine --reka-toast-swipe-move-[x|y] and --reka-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:
<template>
<ToastProvider swipe-direction="right">
<ToastRoot class="ToastRoot">
...
</ToastRoot>
<ToastViewport />
</ToastProvider>
</template>/* styles.css */
.ToastRoot[data-swipe='move'] {
transform: translateX(var(--reka-toast-swipe-move-x));
}
.ToastRoot[data-swipe='cancel'] {
transform: translateX(0);
transition: transform 200ms ease-out;
}
.ToastRoot[data-swipe='end'] {
animation: slideRight 100ms ease-out;
}
@keyframes slideRight {
from {
transform: translateX(var(--reka-toast-swipe-end-x));
}
to {
transform: translateX(100%);
}
}Accessibility
Adheres to the aria-live requirements.
Sensitivity
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.
<template>
<ToastRoot type="foreground">
<ToastDescription>File removed successfully.</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
<ToastRoot type="background">
<ToastDescription>We've just released Reka UI 2.0.</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>Alternative action
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.
<template>
<ToastRoot type="background">
<ToastTitle>Upgrade Available!</ToastTitle>
<ToastDescription>We've just released Reka UI 2.0.</ToastDescription>
<ToastAction alt-text="Goto account settings to upgrade">
Upgrade
</ToastAction>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
<ToastRoot type="foreground" :duration="10000">
<ToastDescription>File removed successfully.</ToastDescription>
<ToastAction alt-text="Undo (Alt+U)">
Undo <kbd>Alt</kbd>+<kbd>U</kbd>
</ToastAction>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>Close icon button
When providing an icon (or font icon), remember to label it correctly for screen reader users.
<template>
<ToastRoot type="foreground">
<ToastDescription>Saved!</ToastDescription>
<ToastClose aria-label="Close">
<span aria-hidden="true">×</span>
</ToastClose>
</ToastRoot>
</template>Keyboard Interactions
| Key | Description |
|---|---|
F8 | Focuses toasts viewport. |
Tab | Moves focus to the next focusable element. |
Shift + Tab | Moves focus to the previous focusable element. |
Space |
When focus is on a ToastAction, closes the toast when closeOnClick is true. When focus is on ToastClose, closes the toast.
|
Enter |
When focus is on a ToastAction, closes the toast when closeOnClick is true. When focus is on ToastClose, closes the toast.
|
Esc |
When focus is on a Toast, closes the toast
|
Custom APIs
Abstract parts
Create your own API by abstracting the primitive parts into your own component.
Usage
<script setup lang="ts">
import Toast from './your-toast.vue'
</script>
<template>
<Toast
title="Upgrade available"
content="We've just released Radix 3.0!"
>
<button @click="handleUpgrade">
Upgrade
</button>
</Toast>
</template>Implementation
// your-toast.vue
<script setup lang="ts">
import { ToastAction, ToastClose, ToastDescription, ToastRoot, ToastTitle } from 'reka-ui'
defineProps<{
title: string
content: string
}>()
</script>
<template>
<ToastRoot>
<ToastTitle v-if="title">
{{ title }}
</ToastTitle>
<ToastDescription v-if="content">
{{ content }}
</ToastDescription>
<ToastAction
as-child
alt-text="toast"
>
<slot />
</ToastAction>
<ToastClose aria-label="Close">
<span aria-hidden="true">×</span>
</ToastClose>
</ToastRoot>
</template>Imperative API
To add toasts by calling a function, use the toast manager. The pattern below shows a single toast component instead.
Usage
<script setup lang="ts">
import Toast from './your-toast.vue'
const savedRef = ref<InstanceType<typeof Toast>>()
</script>
<template>
<div>
<form @submit="savedRef.publish()">
...
</form>
<Toast ref="savedRef">
Saved successfully!
</Toast>
</div>
</template>Implementation
// your-toast.vue
<script setup lang="ts">
import { ToastClose, ToastDescription, ToastRoot, ToastTitle } from 'reka-ui'
import { ref } from 'vue'
const count = ref(0)
function publish() {
count.value++
}
defineExpose({
publish
})
</script>
<template>
<ToastRoot
v-for="index in count"
:key="index"
>
<ToastDescription>
<slot />
</ToastDescription>
<ToastClose>Dismiss</ToastClose>
</ToastRoot>
</template>