Slide Over Panel
非模態的窄版浮動面板,可拖曳磁吸至左右任一側,快甩即收合為邊緣把手,全程彈簧跟手。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add slide-over-panelSlide Over Panel 是用 React、Tailwind CSS 與 Motion 做的非模態浮動側邊面板(floating side panel):可以拖曳到左側或右側並自動吸附,甩動即收合成邊緣把手,打開時不鎖捲動,也不擋住底下內容的操作。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<SlideOverPanel/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 編輯器或設計工具裡的屬性、圖層面板,使用者需要一邊看面板一邊操作畫布。
- Dashboard 的近期動態、通知或篩選條件側欄,可以隨時收到邊緣。
- 地圖或資料視覺化頁面上的圖例與說明,讓使用者自己決定放左邊或右邊。
- 文件或後台頁面的輔助資訊,例如 AI 助理對話、待辦清單。
- 不適合:需要遮罩、強制使用者先處理完再繼續的流程,改用 Drawer。
安裝
npx webberui@latest add https://webberui.com/r/slide-over-panel.json或在 components.json 設定 registries 後,改用 @webberui/slide-over-panel 安裝。
使用
面板以 absolute 定位,anchor="container"(預設)時會貼齊最近的定位祖先,請確保外層容器為 position: relative:
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
<div className="relative h-[480px] overflow-hidden">
{/* 你的主要內容 */}
<SlideOverPanel title="近期動態">
<p>面板內容…</p>
</SlideOverPanel>
</div>若要讓面板浮於整個視口,改用 anchor="viewport"(採 fixed 定位,不需定位祖先)。
範例
受控收合:從工具列按鈕切換
傳入 collapsed 與 onCollapsedChange 由外部管理狀態;使用者拖曳或甩動把手時,回呼也會觸發,狀態保持同步。
"use client";
import * as React from "react";
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
export function EditorWithPanel() {
const [collapsed, setCollapsed] = React.useState(false);
return (
<div className="relative h-[560px] overflow-hidden rounded-xl border">
<div className="p-4">
<button onClick={() => setCollapsed((c) => !c)} className="rounded-md border px-3 py-1.5 text-sm">
{collapsed ? "顯示屬性面板" : "隱藏屬性面板"}
</button>
</div>
<SlideOverPanel
title="屬性"
collapsed={collapsed}
onCollapsedChange={setCollapsed}
>
<dl className="space-y-2 text-sm">
<div className="flex justify-between"><dt>寬度</dt><dd>320 px</dd></div>
<div className="flex justify-between"><dt>高度</dt><dd>180 px</dd></div>
</dl>
</SlideOverPanel>
</div>
);
}記住使用者選的那一側
非受控模式下用 defaultSide 給初始值,再以 onSideChange 把使用者的選擇存起來,下次載入時帶回。
"use client";
import { SlideOverPanel } from "@/components/ui/slide-over-panel";
export function FiltersPanel({ initialSide }: { initialSide: "left" | "right" }) {
return (
<SlideOverPanel
title="篩選條件"
defaultSide={initialSide}
onSideChange={(side) => localStorage.setItem("filters-panel-side", side)}
panelWidth={260}
>
{/* 篩選表單 */}
</SlideOverPanel>
);
}浮在整個視窗上、預設收合
anchor="viewport" 改用 fixed 定位,不需要定位祖先;defaultCollapsed 讓頁面載入時只露出把手。
<SlideOverPanel
anchor="viewport"
defaultCollapsed
label="說明面板"
title="使用說明"
>
<p className="text-sm">拖曳把手可以把面板移到另一側。</p>
</SlideOverPanel>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 面板內容 |
title | React.ReactNode | — | 面板抬頭標題,同時作為無障礙標籤來源 |
side | "left" | "right" | — | 受控貼齊側 |
defaultSide | "left" | "right" | "right" | 非受控預設貼齊側 |
onSideChange | (side) => void | — | 貼齊側變更回呼 |
collapsed | boolean | — | 受控收合狀態 |
defaultCollapsed | boolean | false | 非受控預設收合狀態 |
onCollapsedChange | (collapsed) => void | — | 收合狀態變更回呼 |
panelWidth | number | 300 | 面板本體寬度(px,不含把手) |
anchor | "container" | "viewport" | "container" | 定位基準 |
label | string | — | 無障礙標籤,未提供時回退到 title |
className | string | — | 追加到面板本體的 className |
細節
- 磁吸貼側:拖放後依面板中心落在容器左右哪一半,彈簧吸附到該側並展開。
- 快甩收合:放開時水平速度超過門檻(600 px/s),即朝甩動方向收合為邊緣把手;反向拉出把手則重新展開。
- 全程跟手:拖曳、回彈、磁吸、收合皆走同一組 spring,落點俐落不生硬。
- 受控雙軸:
side與collapsed皆支援受控/非受控雙模式,可各自獨立控制。
可及性
- 面板為
role="region",以title或label作為aria-label。 - 邊緣把手為原生
button:Enter/Space切換收合,←/→磁吸至左右側並展開,補足拖曳無法用鍵盤操作的缺口。 - 把手以
aria-expanded反映展開狀態;收合時面板本體帶aria-hidden並停用互動。 - 面板為非模態,不鎖捲動、不攔截底層內容的互動(
pointer-events僅作用於面板本身)。 - 使用者系統開啟「減少動態效果」時,停用拖曳並改以瞬間定位取代彈簧位移。
常見問題
可以在 Next.js App Router 使用嗎?
可以。原始碼已有 "use client",在 Server Component 頁面直接 import 即可。面板會在量測到容器寬度後才顯示並滑入,伺服器端輸出時是透明的。
為什麼面板沒有出現在預期的位置?
預設 anchor="container" 時面板以 absolute inset-0 貼齊最近的定位祖先,外層容器需要 position: relative 與明確的高度。要貼齊整個視窗請改用 anchor="viewport"。
打開面板時,底下的內容還能操作嗎?
可以。外層舞台設定為 pointer-events-none,只有面板本身接收滑鼠事件,也不會鎖住頁面捲動。
只能用滑鼠拖曳嗎?
不是。聚焦邊緣把手後,Enter 或 Space 切換收合,←、→ 吸附到左側或右側並展開。開啟減少動態效果時會停用拖曳,改為直接定位。
怎麼調整寬度和外觀?
panelWidth 設定面板本體寬度(px,預設 300,不含 32px 的把手);className 會加到面板本體上,可以覆寫背景、邊框或陰影。