GrToaster
Shows brief pop-up notifications about the result of actions.
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
| Need | Take |
|---|---|
| The message belongs to a place on the page | GrAlert |
| A server error with details | GrResponseErrorBanner |
| An answer from the user is needed | GrConfirmDialog |
| A long process is running | GrProgressBar |
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
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
<GrToaster>Label</GrToaster>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 —
promiserewrites «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 NovemberOpenA scanner that keeps losing the network
The socket retries every second and reports every failure;
dedupeKeyfolds 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 withcollapse,max-visiblecaps the screen, and theactionsslot 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, andfocus()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 withswipe-dismiss.Customers · export
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
placement | GrToasterPlacement | undefined | "top-right" | The corner of the screen for the stack of notifications. |
regionLabel | string | undefined | undefined | The a11y label of the region container (i18n). |
collapse | number | false | undefined | 3 | From 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. |
width | string | number | undefined | undefined | The 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. |
dismissLabel | string | undefined | undefined | The a11y label of the close button (i18n). |
maxVisible | number | undefined | 4 | The maximum of toasts visible at once; the rest wait in the queue. `4` by default. |
focusHotkey | string | 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. |
swipeDismiss | boolean | undefined | true | Swiping 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
| Slot | Type | Description |
|---|---|---|
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
| Methods | Signature | Description |
|---|---|---|
focus | () => boolean | Focus 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 type { GrToasterPlacement, GrToasterProps, GrToastTone } from '@feugene/granularity/components/GrToaster'Filedist/types/components/GrToaster/grToasterStyles.d.ts
type GrToasterPlacement = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';Filedist/types/components/GrToaster/GrToaster.vue.d.ts
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
const grToasterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrToaster">;Filedist/types/components/GrToaster/safelist.d.ts
const grToasterSafelist: string[];Filedist/types/composables/useToast.d.ts
type GrToastTone = GrTone;type ToastAction = {
label: string;
onClick: () => void;type ToastInput = {
title: string;
message?: string;
tone?: GrToastTone;
timeoutMs?: number;
action?: ToastAction;
actions?: ToastAction[];
dedupeKey?: string;
};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
type GrToasterInstance = ComponentExposed<typeof GrToasterComponent>;Accessibility
Full contract of the package: docs/keyboard.md ↗
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.
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.
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.
| Token | Default | What it changes |
|---|---|---|
--gr-toaster-stack-peek | 12px | how far every next card peeks out |
--gr-toaster-stack-scale | 0.05 | by 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.
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:
<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.
Props in this example