Feedback and live
Toast
Short notices that rise in at the edge of the screen and stack in line, with a thin bar that shows how long each one stays.
Live preview
Loading preview
Buttons that send a default toast, a success toast and an upload that goes from loading to success. Replay sends the upload toast again.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add toast-stackUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { Button } from '../button/button';
import { Toaster, toast } from './toast-stack';
import { resetToasts } from './toast-stack-core';
// How long the pretend upload takes (ms).
const UPLOAD = 1600;
function upload() {
toast.promise(new Promise((resolve) => window.setTimeout(resolve, UPLOAD)), {
loading: 'Uploading 3 files',
success: '3 files uploaded',
error: 'Upload failed',
});
}
export default function ToastStackDemo({ replay = 0 }: { replay?: number }) {
React.useEffect(() => () => resetToasts(), []);
React.useEffect(() => {
if (replay === 0) return;
upload();
}, [replay]);
return (
<div className="relative flex min-h-80 w-full flex-col items-center justify-center">
<div className="flex flex-wrap items-center justify-center gap-3">
<Button
size="sm"
hierarchy="secondary"
onClick={() => toast('Link copied', { description: 'Anyone with the link can view this page.' })}
>
Copy link
</Button>
<Button
size="sm"
hierarchy="secondary"
onClick={() =>
toast.success('Changes saved', {
action: { label: 'Undo', onClick: () => toast('Changes undone') },
})
}
>
Save changes
</Button>
<Button size="sm" hierarchy="secondary" onClick={upload}>
Upload files
</Button>
</div>
<Toaster className="absolute right-0 bottom-0 w-[min(360px,100%)]" />
</div>
);
}
API
Parts: toaster (region), stack (list), toast, status icon slot, title (text morph), description, action button, close button, time bar.
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "bottom-right" | "bottom-center" | "bottom-left" | "top-right" | "top-center" | "top-left" | "bottom-right" | Toaster: where the stack sits. |
| visible | number | 3 | Toaster: how many toasts show in the collapsed stack. |
| hotkey | string | "t" | Toaster: with Alt, moves focus to the newest toast. |
| type | "default" | "loading" | "success" | "error" | "info" | "default" | Toast: kind of notice; sets the icon and the live region politeness. |
| title | string | - | Toast: main line. |
| description | string | undefined | Toast: second line. |
| duration | number | 4000 | Toast: time on screen in ms. Loading toasts stay until they settle. |
| action | { label: string; onClick: () => void } | undefined | Toast: one action button, for example Undo. |
| id | string | auto | Toast: reuse an id to update a toast in place. |
| Event | Description |
|---|---|
| dismiss | A toast was closed by the button, a swipe, Escape or its timer. Payload: the toast id. |
| action | The action button was pressed. Payload: the toast id. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| TS1 | Arrive | new toast | translate y | below the edge (toast height + 24px) → 0 | duration-4xl (520)ease-throw | Shows at once |
| TS2 | Arrive | new toast | opacity, blur | 0, blur-text → 1, 0 | duration-xl (380)ease-out | Shows at once |
| TS3 | Arrive | older toasts | translate y, scale | 0, 1 → -10px, 0.95 per step back | duration-4xl (520)ease-out | Moves at once |
| TS4 | Hover or focus | stack | height, scale | collapsed, 0.95 per step → list, 1 | duration-2xl (420)ease-out | Stays still |
| TS15 | Hover or focus | stack | translate y | stacked → list position | duration-4xl (520)ease-out | Stays still |
| TS5 | Hover or focus | toasts behind | content opacity | 0 → 1 | duration-md (260)ease-out | Shows at once |
| TS6 | Type change | status icon slot | width, gap | 0, 0 → 16px, 10px | duration-lg (320)ease-out | Resizes at once |
| TS7 | Type change | status icons | opacity, blur | 0, blur-swap → 1, 0 | duration-md (260)ease-out | Swaps at once |
| TS16 | Type change | status icons | scale | scale-swap → 1 | duration-xl (380)ease-out | Swaps at once |
| TS8 | Loading to success | check mark | stroke drawn | 0% → 100% | duration-xl (380)ease-outdelay 100ms | Shows at once |
| TS9 | Loading to error | toast | translate x | 0 → -4px, 4px, -2px, 0 | duration-xl (380)ease-exit | Stays still |
| TS10 | Type change | title | text morph | Uploading 3 files → 3 files uploaded | duration-3xl (460)ease-out | Swaps at once |
| TS11 | While shown | time bar | scale x | 1 → 0 (pauses on hover, focus or a hidden tab) | prop:duration (4000)ease-linear | Runs as is |
| TS12 | Close or time ends | toast | translate y, scale, blur | 0, 1, 0 → 12px toward the edge, 0.96, 4px | duration-lg (320)ease-out | Hides at once |
| TS17 | Close or time ends | toast | opacity | 1 → 0 | duration-sm (200)ease-out | Hides at once |
| TS13 | Swipe away | toast | translate x | finger position → off the edge (width + 40px) | duration-lg (320)ease-out | Hides at once |
| TS14 | A toast leaves | remaining toasts | translate y | old place → new place (gap closed) | duration-4xl (520)ease-out | Moves at once |
| TS18 | A toast leaves | stack | height | old height → new height | duration-2xl (420)ease-out | Resizes at once |
Accessibility
- The toaster is a labelled region ("Notifications (Alt+T)"); Alt plus the hotkey moves focus to the newest toast.
- Each toast is role="status" with aria-live="polite"; error toasts are role="alert" with aria-live="assertive".
- Escape closes the focused toast; the close button has an accessible name.
- The timer pauses on hover, on focus inside the stack and when the tab is hidden, so people have time to read.
- Toasts hidden behind the newest one are aria-hidden while the stack is collapsed.