Lunia DS

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.

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

Usage

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.

PropTypeDefaultDescription
position"bottom-right" | "bottom-center" | "bottom-left" | "top-right" | "top-center" | "top-left""bottom-right"Toaster: where the stack sits.
visiblenumber3Toaster: how many toasts show in the collapsed stack.
hotkeystring"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.
titlestring-Toast: main line.
descriptionstringundefinedToast: second line.
durationnumber4000Toast: time on screen in ms. Loading toasts stay until they settle.
action{ label: string; onClick: () => void }undefinedToast: one action button, for example Undo.
idstringautoToast: reuse an id to update a toast in place.
EventDescription
dismissA toast was closed by the button, a swipe, Escape or its timer. Payload: the toast id.
actionThe 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
TS1Arrivenew toasttranslate ybelow the edge (toast height + 24px) → 0duration-4xl (520)ease-throwShows at once
TS2Arrivenew toastopacity, blur0, blur-text → 1, 0duration-xl (380)ease-outShows at once
TS3Arriveolder toaststranslate y, scale0, 1 → -10px, 0.95 per step backduration-4xl (520)ease-outMoves at once
TS4Hover or focusstackheight, scalecollapsed, 0.95 per step → list, 1duration-2xl (420)ease-outStays still
TS15Hover or focusstacktranslate ystacked → list positionduration-4xl (520)ease-outStays still
TS5Hover or focustoasts behindcontent opacity0 → 1duration-md (260)ease-outShows at once
TS6Type changestatus icon slotwidth, gap0, 0 → 16px, 10pxduration-lg (320)ease-outResizes at once
TS7Type changestatus iconsopacity, blur0, blur-swap → 1, 0duration-md (260)ease-outSwaps at once
TS16Type changestatus iconsscalescale-swap → 1duration-xl (380)ease-outSwaps at once
TS8Loading to successcheck markstroke drawn0% → 100%duration-xl (380)ease-outdelay 100msShows at once
TS9Loading to errortoasttranslate x0 → -4px, 4px, -2px, 0duration-xl (380)ease-exitStays still
TS10Type changetitletext morphUploading 3 files → 3 files uploadedduration-3xl (460)ease-outSwaps at once
TS11While showntime barscale x1 → 0 (pauses on hover, focus or a hidden tab)prop:duration (4000)ease-linearRuns as is
TS12Close or time endstoasttranslate y, scale, blur0, 1, 0 → 12px toward the edge, 0.96, 4pxduration-lg (320)ease-outHides at once
TS17Close or time endstoastopacity1 → 0duration-sm (200)ease-outHides at once
TS13Swipe awaytoasttranslate xfinger position → off the edge (width + 40px)duration-lg (320)ease-outHides at once
TS14A toast leavesremaining toaststranslate yold place → new place (gap closed)duration-4xl (520)ease-outMoves at once
TS18A toast leavesstackheightold height → new heightduration-2xl (420)ease-outResizes 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.