覆蓋層與回饋

Dynamic Island Toast

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

這是 WebberUI Pro 元件

下方的安裝指令需要授權才能執行,未授權時會回 401。

怎麼安裝 Pro 元件 →查看方案 →

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add "https://webberui.com/r/dynamic-island-toast.json?t=<安裝 token>"

還沒有專案,或安裝遇到問題?看快速開始 →

Playground

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

4000
4
16
編輯中的呼叫預設屬性
<DynamicIslandToastProvider/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

安裝

npx webberui@latest add "https://webberui.com/r/dynamic-island-toast.json?t=<安裝 token>"

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

使用

在應用最外層(或需要通知的範圍外層)包一次 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 內建,預設經 createPortal 以 fixed 掛到 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 就內容尺寸伸縮,內容以 AnimatePresence(popLayout)交叉淡入,營造「一團黑塊流動變形」的靈動島質感。
  • 堆疊景深:前景固定為最新那則;較舊的通知在島身後方以絕對定位往下露出一小條邊(每層 translateY 7px、scale −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,不會被輔助科技朗讀
  • 使用者系統開啟「減少動態效果」時,形變、堆疊位移與呼吸光點全部歸零,退化為短暫的淡入淡出,不產生任何位移感
  • 通知不會搶焦點,推送後焦點停留在原本的操作元素上