Reka UI logoReka
backdrop
Components

Toast

A succinct message that is displayed temporarily.
  • 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.

sh
$ npm add reka-ui

Anatomy

Import the component.

vue
<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 AttributeValue
[data-expanded]Present while the pointer is over the viewport or focus is inside it.
CSS VariableDescription
--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.

tip
Built with Presence component - supports any animation techniques while maintaining access to presence emitted events.
Data AttributeValue
[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 VariableDescription
--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.

vue
<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 AttributeValue
[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.

PropDefaultType
toasts
Ref<ToastObject[]>
The toasts, newest first. Closed toasts stay in the list until their exit animation ends.
add
(options: ToastAddOptions) => string
Adds 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)) => void
Updates an open toast with new options, or with a function of the previous toast.
close
(id?: string) => void
Closes 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:

PropDefaultType
id
string
The unique identifier. Generated when not given.
title
string
Rendered by ToastTitle when it has no slot content.
description
string
Rendered 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' | string
Exposed as data-status. loading toasts are never dismissed automatically.
duration
number
Time 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
ToastPositionerOptions
Props for ToastPositioner, including the anchor element.
data
Data
Custom data for your own rendering.
onClose
() => void
Called when the toast closes.
onRemove
() => void
Called 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.

vue
<!-- 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>
vue
<!-- App.vue -->
<template>
  <ToastProvider>
    <SaveButton />
    <Toaster />
  </ToastProvider>
</template>
vue
<!-- 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.

ts
// toast.ts
import { createToastManager } from 'reka-ui'

export const toast = createToastManager()
vue
<!-- App.vue -->
<script setup lang="ts">
import { toast } from './toast'
</script>

<template>
  <ToastProvider :toast-manager="toast">
    <RouterView />
    <Toaster />
  </ToastProvider>
</template>
ts
// 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.

ts
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.

ts
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.

vue
<template>
  <ToastProvider :limit="3">
    <Toaster />
  </ToastProvider>
</template>
css
.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.

vue
<!-- 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>
ts
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.

vue
<template>
  <ToastProvider>
    ...
    <ToastViewport :hotkey="['altKey', 'KeyT']" />
  </ToastProvider>
</template>

Custom duration

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

vue
<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.

vue
<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:

vue
<template>
  <ToastProvider swipe-direction="right">
    <ToastRoot class="ToastRoot">
      ...
    </ToastRoot>
    <ToastViewport />
  </ToastProvider>
</template>
css
/* 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.

vue
<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.

vue
<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.

vue
<template>
  <ToastRoot type="foreground">
    <ToastDescription>Saved!</ToastDescription>
    <ToastClose aria-label="Close">
      <span aria-hidden="true">×</span>
    </ToastClose>
  </ToastRoot>
</template>

Keyboard Interactions

KeyDescription
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

vue
<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

vue
// 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

vue
<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

vue
// 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>