WebberUI

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.jsonfiles[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>

每一層都是一個 DrillLevelid / title / content)。content 內可再放 DrillTrigger 或呼叫 useDrill(),形成任意深度的鑽入。

受控模式

傳入 stackonStackChange 即進入受控模式,由外部完全掌握堆疊;不傳則用 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型別預設值說明
rootReact.ReactNode最底層(根)面板的內容,永遠可見
rootLabelReact.ReactNode"首頁"根層在麵包屑中顯示的標籤
stackDrillLevel[]受控模式:目前的面板堆疊
defaultStackDrillLevel[][]非受控模式:初始面板堆疊
onStackChange(stack: DrillLevel[]) => void堆疊變動時的回呼(push / pop 皆觸發)
breadcrumbsbooleantrue是否顯示頂端麵包屑列
recedenumber0.06每往下一層縮退的比例
parallaxnumber44每往下一層向左位移的距離(px),形成視差
dimnumber0.14每往下一層疊加的調暗不透明度
maxDimnumber0.55疊暗不透明度的上限
durationnumber0.42面板推入 / 回退的動畫時長(秒)
closeOnEscapebooleantrue按 Escape 時回退一層
syncParamstring要同步的 URL query 參數名稱
syncHistorybooleanfalsepushState 讓瀏覽器上一頁可逐層回退(預設 replaceState
resolveLevel(id: string) => DrillLevel | nullid 還原層級,支援重新整理 / 深連結還原
classNamestring附加到最外層容器的 class
panelClassNamestring套用在每個面板容器上的 class

DrillTrigger

繼承原生 <button> 屬性(onClick 除外)。

Prop型別預設值說明
levelDrillLevel點擊後要推入的面板
childrenReact.ReactNode按鈕內容(右側自動附上鑽入箭號)

useDrill()

DrillStackPanels 內部呼叫,回傳鑽入控制器:

欄位型別說明
stackDrillLevel[]目前已開啟的面板堆疊(不含根層)
depthnumber堆疊深度
activeIdstring | 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" 且停用點擊

On this page