Drill Stack Panels
層層鑽入的詳情面板系統,新面板從右推入、下層縮退帶視差調暗,麵包屑可逐層回退並支援 URL 同步。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
0.06
44
0.14
0.42
<DrillStackPanels />
安裝
npx shadcn@latest add https://webberui.com/r/drill-stack-panels.json或在 components.json 設定 registries 後,改用 @webberui/drill-stack-panels 安裝。
安裝依賴後,從 registry JSON(/r/drill-stack-panels.json 的 files[0].content)複製 drill-stack-panels.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion lucide-react clsx tailwind-merge使用
root 是永遠可見的最底層。在任何面板內容中呼叫 useDrill() 取得 push 往下鑽一層;或直接用便利元件 DrillTrigger,點一下就推入指定的 level。麵包屑與每個面板頁首的返回鍵負責逐層回退。
import {
DrillStackPanels,
DrillTrigger,
useDrill,
type DrillLevel,
} from "@/components/ui/drill-stack-panels";
function ProfilePanel() {
const { push } = useDrill();
const detail: DrillLevel = {
id: "detail",
title: "詳情",
content: <p className="p-4">最深一層的內容。</p>,
};
return (
<button onClick={() => push(detail)} className="p-4">
查看詳情
</button>
);
}
<div className="h-[360px]">
<DrillStackPanels
rootLabel="首頁"
root={
<DrillTrigger
level={{ id: "profile", title: "個人資料", content: <ProfilePanel /> }}
>
個人資料
</DrillTrigger>
}
/>
</div>每一層都是一個 DrillLevel(id / title / content)。content 內可再放 DrillTrigger 或呼叫 useDrill(),形成任意深度的鑽入。
受控模式
傳入 stack 與 onStackChange 即進入受控模式,由外部完全掌握堆疊;不傳則用 defaultStack 的非受控模式(內部自管狀態)。
const [stack, setStack] = React.useState<DrillLevel[]>([]);
<DrillStackPanels root={root} stack={stack} onStackChange={setStack} />;URL 同步
設定 syncParam 把目前的鑽入路徑寫進網址 query(值為各層 id)。搭配 resolveLevel(由 id 還原 DrillLevel)即可支援重新整理與深連結還原;再加上 syncHistory 則讓瀏覽器「上一頁」逐層回退。
<DrillStackPanels
root={root}
syncParam="path"
syncHistory
resolveLevel={(id) => LEVELS[id] ?? null}
/>Props
DrillStackPanels
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
root | React.ReactNode | — | 最底層(根)面板的內容,永遠可見 |
rootLabel | React.ReactNode | "首頁" | 根層在麵包屑中顯示的標籤 |
stack | DrillLevel[] | — | 受控模式:目前的面板堆疊 |
defaultStack | DrillLevel[] | [] | 非受控模式:初始面板堆疊 |
onStackChange | (stack: DrillLevel[]) => void | — | 堆疊變動時的回呼(push / pop 皆觸發) |
breadcrumbs | boolean | true | 是否顯示頂端麵包屑列 |
recede | number | 0.06 | 每往下一層縮退的比例 |
parallax | number | 44 | 每往下一層向左位移的距離(px),形成視差 |
dim | number | 0.14 | 每往下一層疊加的調暗不透明度 |
maxDim | number | 0.55 | 疊暗不透明度的上限 |
duration | number | 0.42 | 面板推入 / 回退的動畫時長(秒) |
closeOnEscape | boolean | true | 按 Escape 時回退一層 |
syncParam | string | — | 要同步的 URL query 參數名稱 |
syncHistory | boolean | false | 用 pushState 讓瀏覽器上一頁可逐層回退(預設 replaceState) |
resolveLevel | (id: string) => DrillLevel | null | — | 由 id 還原層級,支援重新整理 / 深連結還原 |
className | string | — | 附加到最外層容器的 class |
panelClassName | string | — | 套用在每個面板容器上的 class |
DrillTrigger
繼承原生 <button> 屬性(onClick 除外)。
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
level | DrillLevel | — | 點擊後要推入的面板 |
children | React.ReactNode | — | 按鈕內容(右側自動附上鑽入箭號) |
useDrill()
在 DrillStackPanels 內部呼叫,回傳鑽入控制器:
| 欄位 | 型別 | 說明 |
|---|---|---|
stack | DrillLevel[] | 目前已開啟的面板堆疊(不含根層) |
depth | number | 堆疊深度 |
activeId | string | null | 最上層面板 id;為空時 null(根層) |
push | (level: DrillLevel) => void | 推入一層(id 重複時忽略) |
pop | () => void | 回退一層 |
popTo | (id: string | null) => void | 跳回指定層;null 回到根層 |
reset | () => void | 收合所有面板回到根層 |
細節
- 每開一層,新面板從右側整寬推入;所有下層同時往左位移(視差)、等比縮退並疊加調暗遮罩,越深越暗(上限
maxDim),營造層層堆疊的縱深 - 推入起點以
ResizeObserver量測的面板視口寬度為準(px),避免%與px混用造成動畫跳動;視口尺寸變動即時更新 - 回退由
AnimatePresence逐層退場,麵包屑項目也同步依序彈出;點任一麵包屑用popTo直接跳回該層,根層標籤則收合全部 - 受控 / 非受控雙模式:傳
stack走受控,否則以defaultStack內部自管 - URL 同步將堆疊路徑編碼進 query 參數;
resolveLevel提供時可從網址完整還原,syncHistory則接上瀏覽器上一頁 / 下一頁
可及性
- 使用者系統開啟「減少動態效果」時,推入 / 縮退 / 調暗過渡的時長全部歸零,直接切換到定位;版面與功能不受影響
- 推入新層時焦點自動移到最上層面板(
preventScroll),回退與初次掛載不搶焦點 - 非最上層的面板標記
aria-hidden並停用指標事件,輔助科技與滑鼠只會落在當前作用中的面板 - 每個面板為
role="group"並以標題作aria-label;返回鍵有aria-label="返回上一層",closeOnEscape時按 Escape 即回退一層 - 麵包屑為原生
<nav><ol>結構,各層是可聚焦按鈕,當前層標記aria-current="page"且停用點擊