WebberUI

Dynamic Island Toast

iOS 靈動島式通知:待命膠囊液態展開為通知卡片,多則通知在後方堆疊露邊,支援 loading → success 就地更新。

載入預覽⋯

Playground

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

4000
4
16
<DynamicIslandToast />

安裝

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

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

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

npm install motion lucide-react clsx tailwind-merge

使用

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

import {
  DynamicIslandToastProvider,
  useDynamicIslandToast,
} from "@/components/ui/dynamic-island-toast";

function App() {
  return (
    <DynamicIslandToastProvider duration={4000} max={4}>
      <SaveButton />
    </DynamicIslandToastProvider>
  );
}

function SaveButton() {
  const { notify } = useDynamicIslandToast();

  return (
    <button
      onClick={() =>
        notify({
          title: "部署完成",
          description: "已上線至正式環境",
          variant: "success",
        })
      }
    >
      部署
    </button>
  );
}

notify() 會回傳該通知的 id,搭配 update() 可以把同一則 loading 就地換成結果,dismiss() 則提前關閉:

const { notify, update, dismiss } = useDynamicIslandToast();

const id = notify({ title: "同步中…", variant: "loading" });
// 流程結束:同一顆島身直接形變成 success,不會另開一則
update(id, { title: "同步完成", variant: "success" });
// 或手動關閉
dismiss(id);

島身由 DynamicIslandToastProvider 內建,預設經 createPortalfixed 掛到 document.body;傳入 container(需 position: relative)則改以 absolute 掛在該容器內,適合侷限在展示區塊。

Props

DynamicIslandToastProvider

Prop型別預設值說明
childrenReact.ReactNode應用內容,useDynamicIslandToast() 需在此範圍內呼叫
durationnumber4000自動關閉的毫秒數;設為 0 以下或 Infinity 則不自動關閉
maxnumber4同時最多保留的通知數,超過時擠掉最舊的一則
align'start' | 'center' | 'end''center'島身的水平對齊
offsetnumber16距容器頂端的位移(px)
persistentbooleantrue沒有通知時是否保留待命膠囊(帶呼吸光點)
containerHTMLElement | null渲染目標容器;提供時以 absolute 掛入(容器需 relative),未提供則 fixed 掛到 document.body

useDynamicIslandToast()

回傳 { notify, update, dismiss }

方法簽名說明
notify(options: IslandToastOptions) => string推送一則通知,回傳其 id
update(id: string, options: Partial<IslandToastOptions>) => void依 id 更新既有通知並重排倒數
dismiss(id: string) => void依 id 手動關閉某則通知

IslandToastOptions

欄位型別預設值說明
titlestring標題(主要訊息,單行截斷)
descriptionstring說明文字,顯示在標題下方
variant'default' | 'success' | 'error' | 'loading''default'前導圖示(Bell / CheckCircle2 / XCircle / 旋轉 Loader2)
iconReact.ReactNode自訂前導圖示,覆蓋 variant 的預設圖示
durationnumber繼承 Provider覆寫此則的自動關閉毫秒數

細節

  • 液態形變:待命膠囊與通知卡片是同一顆 motion.div,透過 Motion 的 layout 讓島身以 spring 就內容尺寸伸縮,內容以 AnimatePresencepopLayout)交叉淡入,營造「一團黑塊流動變形」的靈動島質感。
  • 堆疊景深:前景固定為最新那則;較舊的通知在島身後方以絕對定位往下露出一小條邊(每層 translateY 7pxscale −0.05、透明度遞減),最多再露 2 層,其餘僅計入而不顯示。
  • loading 生命週期variant: "loading" 預設常駐不倒數(顯示旋轉圖示),直到 update() 換成結果或 dismiss()update() 讓同一顆島身直接形變,避免另開新通知造成閃爍。
  • 倒數暫停:滑鼠移入或鍵盤焦點進入島身時清除所有 setTimeout;離開後為每則非 loading 通知重排一次完整倒數(重設而非續跑)。被 dismiss、被 max 擠掉或 Provider 卸載時,對應的 timer 一律回收。
  • 就地掛載:預設 fixed 貼齊視窗頂端置中;傳入 container 則改 absolute 掛進指定容器,便於在文件流中的區塊內展示。

可及性

  • 島身容器帶 aria-live="polite"、每則通知帶 role="status",新通知與 update() 的內容變更都會由螢幕閱讀器以不打斷目前操作的方式朗讀
  • 每則通知都有帶 aria-label 的關閉鈕;鍵盤焦點進入島身時等同 hover——暫停倒數,焦點離開後恢復
  • 待命膠囊的呼吸光點與堆疊邊皆 aria-hidden,不會被輔助科技朗讀
  • 使用者系統開啟「減少動態效果」時,形變、堆疊位移與呼吸光點全部歸零,退化為短暫的淡入淡出,不產生任何位移感
  • 通知不會搶焦點,推送後焦點停留在原本的操作元素上

On this page