GrToaster

  • core
  • feedback
  • API · 8 props
  • ⌨ keys

Shows brief pop-up notifications about the result of actions.

import { GrToaster } from '@feugene/granularity/components/GrToaster'

Overview

When to take it

  • an action has finished — “saved”, “copied”, “the letter has been sent”: a confirmation must not take up room;
  • the message comes from anywhere — the queue lives in useToast, and it can be called from any module;
  • the message has an undo — an action button in a toast replaces a confirmation dialog for a reversible action;
  • there may be several messages — a stack with a queue, a pause under the cursor and progress until closing.

When to take something else

NeedTake
The message belongs to a place on the pageGrAlert
A server error with detailsGrResponseErrorBanner
An answer from the user is neededGrConfirmDialog
A long process is runningGrProgressBar

One GrToaster is placed per application: the state lives in useToast, and a second instance would show the same queue twice. A critical message is not delivered with a toast — it will leave on its own, and the user may not see it.

Playground controls come from the API, not from hand

Stage

This component lives above the screen, not inside a canvas: a screen opens it, and the playground has nothing to open it from. The examples below show it at work.

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • What an invoice action leaves behind

    A reminder is confirmed and goes by itself. Voiding is reversible, so there is no confirmation dialog: the toast carries «Undo» and stays eight seconds to use it. The PDF is one toast for the whole request — promise rewrites «Preparing» into the result in place, and the promise comes back unswallowed, the failure still ours to catch.

    INV-2026-0418 · Northwind Traders€1,284.50 · due 1 NovemberOpen
  • A scanner that keeps losing the network

    The socket retries every second and reports every failure; dedupeKey folds the repeats into one live toast and restarts its timer, and the «back online» toast takes its place under the same key. Five scans make five confirmations: from the third the stack folds into a pile with collapse, max-visible caps the screen, and the actions slot gives a warning «Retry now» and a received carton «Undo scan».

    Receiving · Dock 3Online — scans are sent at once
  • Reaching a toast from the keyboard

    An export finishes while the user is deep in a long page. focus-hotkey — F6 by default, the APG key for moving between regions — jumps into the stack from anywhere, and focus() does the same from a button, saying whether there was anything to focus. Delete closes a focused toast, the keyboard twin of a swipe, which is turned off here with swipe-dismiss.

    Customers · export

API generated from the package · cannot drift

Props

PropTypedefaultDescription
placementGrToasterPlacement | undefined"top-right"The corner of the screen for the stack of notifications.
regionLabelstring | undefinedundefinedThe a11y label of the region container (i18n).
collapsenumber | false | undefined3From how many visible toasts the stack collapses into a pile: the column takes the space of one toast, and the rest peek out from under the front one. It unfolds on hover and on focus. `false` — never collapse. A threshold rather than a switch: while there are few toasts, a column reads better than a pile, and there is no reason to change it.
widthstring | number | undefinedundefinedThe width of the stack. A number means pixels, a string any CSS length. It goes into `--gr-toaster-width`, so the same can be set by the theme as well.
dismissLabelstring | undefinedundefinedThe a11y label of the close button (i18n).
maxVisiblenumber | undefined4The maximum of toasts visible at once; the rest wait in the queue. `4` by default.
focusHotkeystring | false | undefined"F6"The key that moves the focus into the stack of notifications; `false` turns the hotkey off. `F6` is the APG recommendation for moving between the "regions" of a page.
swipeDismissboolean | undefinedtrueSwiping a toast towards its edge of the screen. The keyboard equivalent — `Delete` and `Backspace` on a focused toast — stays even with the gesture turned off.

Slots

SlotTypeDescription
actions{ toast: Toast; dismiss: () => void; }The buttons of a toast instead of `action`/`actions` from its description. It receives the `toast` itself and `dismiss` — a function that closes exactly this toast.

Methods

MethodsSignatureDescription
focus() => booleanFocus on the top toast, as with `focusHotkey`; `false` — there are no toasts.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrToasterPlacement, GrToasterProps, GrToastTone } from '@feugene/granularity/components/GrToaster'

Filedist/types/components/GrToaster/grToasterStyles.d.ts

GrToasterPlacementtype
type GrToasterPlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';

Filedist/types/components/GrToaster/GrToaster.vue.d.ts

GrToasterPropsinterface
interface GrToasterProps {
    placement?: GrToasterPlacement;
    maxVisible?: number;
    dismissLabel?: string;
    regionLabel?: string;
    width?: string | number;
    focusHotkey?: string | false;
    swipeDismiss?: boolean;
    collapse?: number | false;
}

Filedist/types/components/GrToaster/config.d.ts

grToasterConfigconst
const grToasterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrToaster">;

Filedist/types/components/GrToaster/safelist.d.ts

grToasterSafelistconst
const grToasterSafelist: string[];

Filedist/types/composables/useToast.d.ts

GrToastTonetype
type GrToastTone = GrTone;
ToastActiontype
type ToastAction = {
    label: string;
    onClick: () => void;
ToastInputtype
type ToastInput = {
    title: string;
    message?: string;
    tone?: GrToastTone;
    timeoutMs?: number;
    action?: ToastAction;
    actions?: ToastAction[];
    dedupeKey?: string;
};
Toasttype
type Toast = {
    id: string;
    title: string;
    message?: string;
    tone: GrToastTone;
    timeoutMs: number;
    action?: ToastAction;
    actions?: ToastAction[];
    dedupeKey?: string;
};

Filedist/types/components/GrToaster/index.d.ts

GrToasterInstancetype
type GrToasterInstance = ComponentExposed<typeof GrToasterComponent>;

Accessibility

Usage from the library prose

A request in a single toast

toast.promise runs the whole lifecycle of a request in one toast: “loading” is rewritten into the result rather than being closed for the sake of a new one — the stack does not jerk.

ts
const { promise } = useToast()

promise(api.sync(), {
  loading: { title: 'Syncing', message: 'Sending the changes…' },
  success: result => ({ title: 'Done', message: `${result.files} files` }),
  error: reason => ({ title: 'It did not work', message: String(reason) }),
})

A string instead of an object is a shortcut for { title }. The loading toast is shown without auto-closing, and the result gets the ordinary timeout.

The promise is returned as it is and a rejection is not swallowed — a toast does not replace the handling of an error, and the catch is still on the caller. If the user closed the toast by hand while the request was running, the result will not resurrect it.

A toast is changed pointwise through update(id, patch): it also restarts the timer when a timeoutMs arrives in the patch.

How many toasts the queue remembers

The maxVisible of the component limits only the visible toasts. The queue itself is bounded by the ceiling of the state — 20 by default; on overflow the oldest are evicted together with their timers. Otherwise a stream of events (a socket reconnecting, a loop of errors) would pile up the queue and dump it on the user when the stack freed up.

ts
app.use(granularityToastPlugin, { maxToasts: 50 })
A pile instead of a column

maxVisible holds the number of visible toasts but says nothing about the room they take up: raised to eight, it gave the application a column half a screen tall on top of the very interface the notifications are shown for.

collapse (3 by default) sets from how many visible toasts the stack folds into a pile: the front card is visible in full, the edges of the next ones peek out from under it, and the whole stack takes up the room of a single toast. collapse: false switches the folding off entirely.

A threshold rather than a switch: while there are two or three toasts, a column reads better than a pile — all of the messages are visible at once, and there is no reason to hide them.

The pile is unfolded by hover and focus — the same two signals that pause the timers (see “The pause on auto-closing”). Once the stack has begun to be read, the countdown has to stop anyway, and introducing a third signal for unfolding would mean pulling those two events apart in time.

The pile grows from its own corner into the depth of the screen: downwards from the upper corners, upwards from the lower ones. The content of the cards behind the front one is dimmed: what is visible through a gap of twelve pixels is the edges of the cards rather than scraps of lines.

TokenDefaultWhat it changes
--gr-toaster-stack-peek12pxhow far every next card peeks out
--gr-toaster-stack-scale0.05by how much it is reduced

The queue is not replaced by this: the pile is about the layout of the visible toasts, the queue — about how many are let be shown at all.

A repeat is folded by a key rather than by remembering the text

The dedupeKey of push() replaces a live toast with the same key and restarts its auto-closing. The typical source of duplicates is navigation replaying the same props of a page: without a key every repetition would start a new toast.

ts
push({ title: flash.success, tone: 'success', dedupeKey: `flash:${flash.success}` })

The key is taken only while the toast is on the screen. That is not the same as remembering the last text shown: such a memory is not reset, and a repeat of an action with the same text (“Saved” a second time) would not be shown at all. Once a toast has closed, the key is free, and the next push is visible again.

A repeat replaces the toast as a whole, like a stage of promise(): the message and the buttons from the previous call are not carried over. A pointed edit of a shown toast is update(id, patch), which has patch semantics.

Announcing toasts to a screen reader

A toast announces itself: role="status", and warning/danger an assertive role="alert". There is no permanent live-region wrapper over the list and there must not be: nesting regions with different assertiveness is not defined by the specification — browsers and screen readers diverge up to losing the announcement entirely. The container remains a named role="region" (regionLabel), which is navigation rather than an announcement.

Swiping

A toast is swiped towards its own edge of the screen: a stack on the right leaves to the right, one on the left to the left. Swiping into the depth of the screen would read as an attempt to get something out from under the toast rather than to throw it away, so the “backwards” movement goes with resistance — the toast follows the finger but does not close.

The threshold is a quarter of the width of the toast, but no less than 56px. If released earlier, the toast returns; if released further, it closes. An interrupted gesture returns the toast: if the pointer was taken by the browser (a system gesture, a call, the loss of the window), the user did not complete the gesture, and finishing it on their behalf is not allowed.

While a toast is being dragged, its timer stands still — otherwise the notification would burn out right under the finger, in the middle of the gesture.

A released toast flies off past its edge and only then closes. The flight is the same transition as in the other states of a toast, so there is nothing to fix for prefers-reduced-motion and no need: the global clamp compresses the transition to instantaneous, and the gesture itself is direct manipulation rather than movement of the interface, and there is nothing to hold back.

swipe-dismiss="false" switches the gesture off; the keyboard equivalent remains in the process.

The keyboard

The toasts are teleported to the end of body, so an “Undo” button lies beyond a reasonable number of Tab presses. F6 (the focusHotkey prop, false switches it off) moves the focus to the top toast, and from there the actions are walked with an ordinary Tab. The toast itself is not a Tab stop of its own — tabindex="-1": a pop-up notification must not intercept the walk over the page.

The same from the outside — through a ref:

vue
<GrToaster ref="toaster" />

focus() returns false if the stack is empty.

Delete and Backspace on a focused toast close it — the same action as a swipe. The focus moves to the neighbouring toast if there is one: there is nowhere to fall onto body in the middle of a stack that is still being read. When the last one is closed, focus returns to where the stack was entered from (F6 or focus() remember that element), if it is still in the document. The cross and the action buttons with dismissOnClick behave the same way when focus is on them. Escape is deliberately not taken — the toaster is not a modal layer, and intercepting it would close a notification instead of the dialog underneath.

The width

width is a number (pixels) or a CSS length; it travels into --gr-toaster-width, so the same thing is set by the theme as well. The default is 360px, and the ceiling is calc(100vw - 2rem).

The pause on auto-closing

The cursor, focus inside the stack or a swipe that has begun stops the timers of all of the visible toasts and freezes the progress bar (WCAG 2.2.1). Toasts from the queue (beyond maxVisible) are always paused — the countdown starts when a toast becomes visible.