Dynamic Island Nav
靈動島式膠囊導航:選中膠囊在項目間滑動形變,選中項目名稱由 0 展開,可再點一次讓島身向下 morph 展開面板。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<DynamicIslandNav />
安裝
npx shadcn@latest add https://webberui.com/r/dynamic-island-nav.json或在 components.json 設定 registries 後,改用 @webberui/dynamic-island-nav 安裝。
安裝依賴後,從 registry JSON(/r/dynamic-island-nav.json 的 files[0].content)複製 dynamic-island-nav.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion lucide-react clsx tailwind-merge使用
以 <DynamicIslandNav> 包住一組 <DynamicIslandNavItem>。每個項目只需 value、icon 與 label;若提供 children,該項目就能在選中後再點一次向下展開靈動島面板。
import { Bell, Home, Search } from "lucide-react";
import {
DynamicIslandNav,
DynamicIslandNavItem,
} from "@/components/ui/dynamic-island-nav";
<DynamicIslandNav defaultValue="home">
<DynamicIslandNavItem value="home" icon={<Home aria-hidden />} label="首頁" />
<DynamicIslandNavItem
value="search"
icon={<Search aria-hidden />}
label="搜尋"
/>
<DynamicIslandNavItem value="alerts" icon={<Bell aria-hidden />} label="通知">
<p>你有 3 則新通知</p>
</DynamicIslandNavItem>
</DynamicIslandNav>受控模式
傳入 value 與 onValueChange 即進入受控模式,選中狀態改由外部管理:
const [value, setValue] = React.useState("home");
<DynamicIslandNav value={value} onValueChange={setValue}>
{/* …items… */}
</DynamicIslandNav>固定於底部
position="fixed-bottom" 會把整座島固定於視窗底部置中(z-50),外層走廊不吃指標事件,適合當作 App 級的浮動導覽:
<DynamicIslandNav position="fixed-bottom" defaultValue="home">
{/* …items… */}
</DynamicIslandNav>Props
DynamicIslandNav
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 一組 <DynamicIslandNavItem> 子項 |
value | string | — | 受控的選中值 |
defaultValue | string | 第一個項目 | 非受控模式的初始選中值 |
onValueChange | (value: string) => void | — | 選中值變更時的回呼 |
position | 'inline' | 'fixed-bottom' | 'inline' | inline 隨文件流;fixed-bottom 固定於視窗底部置中 |
label | string | '導覽' | tablist 的無障礙名稱 |
className | string | — | 附加到膠囊本體的 class |
DynamicIslandNavItem
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | string | — | 項目的唯一識別值 |
icon | React.ReactNode | — | 左側圖示,會被限制在 16px 內 |
label | string | — | 項目名稱,作為 aria-label 並在選中時展開顯示 |
disabled | boolean | false | 停用此項目,無法選取或聚焦 |
children | React.ReactNode | — | 選中時向下展開的面板內容;提供時該項目可展開,未提供則純切換 |
className | string | — | 附加到此項目按鈕的 class |
DynamicIslandNavItem為宣告式設定元件,本身不渲染節點;按鈕與展開面板皆由DynamicIslandNav讀取其 props 統一渲染,因此請務必作為DynamicIslandNav的直接子項使用。
細節
- 選中膠囊滑動:選中背景是一個帶共享
layoutId的motion.span,切換項目時舊項目卸載、新項目掛載同一個layoutId,由 Motion 以 spring 在按鈕之間滑動並同步形變寬度。 - 名稱展開:僅選中項目渲染名稱,寬度自
0動畫到auto(AnimatePresence),帶動整條膠囊左右形變;未選中項目只留圖示。 - 島身展開:選中「有
children的項目」後再點一次,面板高度自0morph 到auto,父膠囊隨內容自然長高,右側箭頭旋轉 180° 標示展開狀態;切到不同項目時key綁定選中值使面板重播進場。 - 展開規則:切到新項目時,有面板即自動展開、無面板則收合;再次點擊目前項目則切換其展開狀態。
可及性
- 膠囊列為
role="tablist",每個項目為role="tab"並帶aria-selected;可展開項目帶aria-expanded與aria-controls,展開面板為role="region"並以aria-labelledby指回對應項目。 - 採 roving tabindex:僅選中項目
tabIndex=0,←/→在項目間移動焦點並選取,Home/End跳到首尾,停用項目自動略過。 - 每個項目都帶
aria-label(即使未選中、名稱未展開時),螢幕閱讀器仍可讀出完整名稱;動畫節點一律aria-hidden,另有role="status"的純文字狀態播報選中與展開狀態。 - 使用者系統開啟「減少動態效果」時,膠囊滑動、名稱展開與面板 morph 全部歸零,退化為即時切換與短暫淡入淡出,不產生位移感。