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.json 的 files[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 | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 應用內容,useToast() 需在此範圍內呼叫 |
position | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-right' | 視口停靠的角落 |
duration | number | 5000 | 自動關閉的毫秒數;設為 0 以下或 Infinity 則不自動關閉 |
max | number | 5 | 同時最多保留的通知數,超過時移除最舊的一張 |
useToast()
回傳 { toast, dismiss }:
| 方法 | 簽名 | 說明 |
|---|---|---|
toast | (options: ToastOptions) => string | 發送一則通知,回傳該通知的 id |
dismiss | (id: string) => void | 依 id 手動關閉某則通知 |
ToastOptions
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
title | string | — | 標題 |
description | string | — | 說明文字,顯示在標題下方 |
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 動畫讓位;退場時往左右邊緣滑出,AnimatePresence的popLayout模式讓剩下的卡片同步收攏補位。 - 倒數暫停:hover 期間清除所有
setTimeout,離開視口時為每張通知重排一次完整倒數(重設而非續跑);被dismiss、被max擠掉或 Provider 卸載時,對應的 timer 一律回收。 - 卡片間距:展開時的間距做成每張卡片自身的 padding 而不是容器的
gap,滑鼠在卡片之間移動時 hover 狀態不會中斷,堆疊不會展開收合來回抖動。 - 視口實作:視口是一條撐滿螢幕直向走廊的
pointer-events-none容器(事件由卡片冒泡上來),容器的螢幕位置固定,popLayout退場時的絕對定位量測才不會因容器高度變化而跳動。
可及性
- 視口帶
aria-live="polite",每張通知帶role="status",新通知會由螢幕閱讀器以不打斷目前操作的方式朗讀 - 每張通知都有帶
aria-label的關閉鈕;鍵盤焦點進入視口時等同 hover——堆疊展開、倒數暫停,焦點離開後恢復 - 使用者系統開啟「減少動態效果」時,滑入滑出與堆疊位移全部歸零,退化為短暫的淡入淡出,不產生任何位移感
- 通知不會搶焦點,發送後焦點停留在原本的操作元素上