WebberUI

Toast Stack

sonner 式堆疊收合通知:新通知從邊緣滑入,未 hover 時收合為一疊卡片,滑入視口展開完整列表並暫停自動關閉倒數。

載入預覽⋯

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

5000
5
<ToastStack />

安裝

npx shadcn@latest add https://webberui.com/r/toast-stack.json

或在 components.json 設定 registries 後,改用 @webberui/toast-stack 安裝。

安裝依賴後,從 registry JSON(/r/toast-stack.jsonfiles[0].content)複製 toast-stack.tsx 原始碼到你的 components/ui/ 目錄:

npm install motion lucide-react clsx tailwind-merge

使用

在應用最外層(或需要通知的範圍外層)包一次 ToastProvider,內部任何元件即可透過 useToast() 發送通知:

import { ToastProvider, useToast } from "@/components/ui/toast-stack";

function App() {
  return (
    <ToastProvider position="bottom-right" duration={5000} max={5}>
      <SaveButton />
    </ToastProvider>
  );
}

function SaveButton() {
  const { toast } = useToast();

  return (
    <button
      onClick={() =>
        toast({
          title: "變更已儲存",
          description: "設定已同步到所有裝置。",
          variant: "success",
        })
      }
    >
      儲存
    </button>
  );
}

toast() 會回傳該通知的 id,搭配 dismiss(id) 可以在流程完成時提前關閉:

const { toast, dismiss } = useToast();

const id = toast({ title: "上傳中…" });
// 完成後手動關閉
dismiss(id);

視口由 ToastProvider 內建,經 createPortal 渲染到 document.body,不需要另外掛 viewport 元件。

Props

ToastProvider

Prop型別預設值說明
childrenReact.ReactNode應用內容,useToast() 需在此範圍內呼叫
position'bottom-right' | 'bottom-left' | 'top-right' | 'top-left''bottom-right'視口停靠的角落
durationnumber5000自動關閉的毫秒數;設為 0 以下或 Infinity 則不自動關閉
maxnumber5同時最多保留的通知數,超過時移除最舊的一張

useToast()

回傳 { toast, dismiss }

方法簽名說明
toast(options: ToastOptions) => string發送一則通知,回傳該通知的 id
dismiss(id: string) => void依 id 手動關閉某則通知

ToastOptions

欄位型別預設值說明
titlestring標題
descriptionstring說明文字,顯示在標題下方
variant'default' | 'success' | 'error''default'變體,決定左緣色帶與圖示(Info / CheckCircle / XCircle)

細節

  • 收合堆疊:未 hover 時只有最前面(最新)的一張留在文件流,其餘卡片絕對定位疊在它後面,依距離遞減 scale(每層 -0.05)並往停靠邊外側位移 14px,露出一小條邊;第 4 張起完全隱藏(透明度 0 且不接收 pointer events)。
  • hover 展開:滑鼠移入視口(或鍵盤焦點進入任何一張通知)時展開成完整列表,後面的卡片從絕對定位切回文件流,由 Motion 的 layout FLIP 以 spring 過渡到各自的位置;移出後反向收攏回堆疊。
  • 進退場:新通知從停靠的上下邊緣滑入(y: ±120% + 淡入),其餘卡片以 layout 動畫讓位;退場時往左右邊緣滑出,AnimatePresencepopLayout 模式讓剩下的卡片同步收攏補位。
  • 倒數暫停:hover 期間清除所有 setTimeout,離開視口時為每張通知重排一次完整倒數(重設而非續跑);被 dismiss、被 max 擠掉或 Provider 卸載時,對應的 timer 一律回收。
  • 卡片間距:展開時的間距做成每張卡片自身的 padding 而不是容器的 gap,滑鼠在卡片之間移動時 hover 狀態不會中斷,堆疊不會展開收合來回抖動。
  • 視口實作:視口是一條撐滿螢幕直向走廊的 pointer-events-none 容器(事件由卡片冒泡上來),容器的螢幕位置固定,popLayout 退場時的絕對定位量測才不會因容器高度變化而跳動。

可及性

  • 視口帶 aria-live="polite",每張通知帶 role="status",新通知會由螢幕閱讀器以不打斷目前操作的方式朗讀
  • 每張通知都有帶 aria-label 的關閉鈕;鍵盤焦點進入視口時等同 hover——堆疊展開、倒數暫停,焦點離開後恢復
  • 使用者系統開啟「減少動態效果」時,滑入滑出與堆疊位移全部歸零,退化為短暫的淡入淡出,不產生任何位移感
  • 通知不會搶焦點,發送後焦點停留在原本的操作元素上

On this page