Drawer
從畫面邊緣滑入的抽屜面板,支援四向、遮罩點擊關閉,並可單軸拖曳滑走關閉。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add drawerDrawer 是用 React、Motion(motion/react)與 Tailwind CSS 做的抽屜(drawer)/側邊面板元件,可以從上、下、左、右四個方向滑入,手機上當成底部彈出面板(bottom sheet)也很順手。面板透過 portal 掛到 document.body,沿關閉方向拖曳就能滑走。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<Drawer/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 手機版網站的漢堡選單或分類導覽,從左側或右側滑出
- 電商列表頁的篩選條件面板,套用後關閉並更新結果
- 行動裝置上的底部操作面板,例如分享、排序或選擇規格
- 後台表格點擊一列後,從右側開出詳細資料或快速編輯
- 不適合:需要停靠在版面旁、與主內容並排存在的檢視器,請改用 Docked Inspector Rail
安裝
npx webberui@latest add https://webberui.com/r/drawer.json或在 components.json 設定 registries 後,改用 @webberui/drawer 安裝。
使用
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerClose,
} from "@/components/ui/drawer";
<Drawer>
<DrawerTrigger>開啟選單</DrawerTrigger>
<DrawerContent side="right" className="w-72">
<div className="flex items-center justify-between border-b border-neutral-200 px-5 py-4 dark:border-neutral-800">
<h2 className="text-base font-semibold">選單</h2>
<DrawerClose />
</div>
<nav className="p-3">{/* 選項 */}</nav>
</DrawerContent>
</Drawer>;DrawerContent 的 side 決定滑入方向("top" | "right" | "bottom" | "left",預設 "right")。面板從對應邊緣以 spring 滑入,點擊遮罩或按 Esc 即可關閉。
傳入 open 與 onOpenChange 可切換為受控模式:
const [open, setOpen] = React.useState(false);
<Drawer open={open} onOpenChange={setOpen}>
{/* ... */}
</Drawer>;範例
手機底部面板
side="bottom" 會讓面板貼齊底部、最高 85vh;用 className 加圓角與拖曳把手的提示。
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerClose,
} from "@/components/ui/drawer";
<Drawer>
<DrawerTrigger>選擇規格</DrawerTrigger>
<DrawerContent side="bottom" className="rounded-t-2xl">
<div className="mx-auto mt-3 h-1.5 w-10 rounded-full bg-neutral-300 dark:bg-neutral-700" />
<div className="flex items-center justify-between px-5 py-4">
<h2 className="text-base font-semibold">選擇規格</h2>
<DrawerClose />
</div>
<div className="grid grid-cols-3 gap-2 px-5 pb-6">
{["S", "M", "L"].map((size) => (
<button key={size} className="rounded-lg border py-2 text-sm">
{size}
</button>
))}
</div>
</DrawerContent>
</Drawer>;篩選面板:套用後關閉
受控模式下,送出表單時自己把 open 設回 false。
"use client";
import * as React from "react";
import { Drawer, DrawerTrigger, DrawerContent } from "@/components/ui/drawer";
export function FilterDrawer({ onApply }: { onApply: (q: FormData) => void }) {
const [open, setOpen] = React.useState(false);
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger>篩選</DrawerTrigger>
<DrawerContent side="left">
<form
className="flex flex-1 flex-col gap-4 p-5"
onSubmit={(e) => {
e.preventDefault();
onApply(new FormData(e.currentTarget));
setOpen(false);
}}
>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" name="inStock" /> 只顯示有庫存
</label>
<button type="submit" className="mt-auto rounded-lg bg-neutral-900 py-2 text-sm text-white">
套用篩選
</button>
</form>
</DrawerContent>
</Drawer>
);
}自訂關閉按鈕內容
DrawerClose 預設顯示 X 圖示,傳入 children 可以換成文字按鈕。
<DrawerContent side="right">
<div className="p-5">{/* 訂單明細 */}</div>
<div className="mt-auto border-t p-4">
<DrawerClose className="w-full py-2 text-sm">繼續購物</DrawerClose>
</div>
</DrawerContent>Props
Drawer
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 內容,需包含 DrawerTrigger 與 DrawerContent |
open | boolean | — | 受控模式的開啟狀態,不傳則由元件內部管理 |
onOpenChange | (open: boolean) => void | — | 開啟狀態變更時的回呼(受控與非受控皆會觸發) |
DrawerTrigger
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | — | 觸發鈕內容 |
className | string | — | 追加到觸發鈕的 className |
DrawerContent
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "right" | 抽屜滑入的方向 |
className | string | — | 追加到面板容器的 className |
children | React.ReactNode | — | 面板內容 |
DrawerClose
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | React.ReactNode | lucide-react X 圖示 | 自訂按鈕內容 |
className | string | — | 追加到關閉按鈕的 className |
細節
- 四向滑入:
side對應面板貼齊的邊緣與初始離場位移。左右向為滿高側欄(w-80 max-w-[85vw]),上下向為滿寬橫幅(max-h-[85vh]),皆可用className覆寫尺寸。 - 拖曳關閉:面板以單軸
drag跟手,只在「關閉方向」放開約束、開啟方向以彈性阻尼回彈。放開時若關閉方向的位移超過 80px 或速度超過 500px/s 即關閉,AnimatePresence的exit從當前拖曳位置接續滑出;未達閾值則由dragSnapToOrigin自動彈回原位。 - Portal 到 body:面板與遮罩經
createPortal渲染到document.body,避免祖先的overflow裁切或transform建立 containing block 影響fixed定位。 - spring 滑入:入場使用 spring(
stiffness: 380, damping: 40),與拖曳彈回共用同一組 spring,手勢與程式動畫的落點手感一致。 - 受控模式:傳入
open即切換為受控,元件不再改動內部狀態;onOpenChange在受控與非受控下都會觸發,可用來監聽開關事件。 - 可調校點:切換色的過渡時長取用
var(--wb-duration-fast, 200ms),未定義時回退 200ms。
可及性
- 面板帶
role="dialog"與aria-modal="true";開啟時焦點移入面板(tabIndex={-1}),關閉後焦點歸還給觸發鈕 - 觸發鈕為原生
<button>,帶aria-haspopup="dialog"與aria-expanded狀態,開啟時以aria-controls連結面板 - 按 Esc 或點擊遮罩皆可關閉;開啟期間
body設為overflow: hidden鎖住背景捲動,關閉時恢復原值 - 使用者系統開啟「減少動態效果」時,滑入與拖曳退化為快速淡入淡出,不產生任何位移感
- 元件走輕量路線,未內建 focus trap;若面板內有多個互動元素且需要完整的鍵盤焦點循環,建議自行搭配 focus trap 方案
常見問題
可以在 Next.js App Router 使用嗎?
可以。檔案開頭已經有 "use client",在 Server Component 頁面裡照常 import 即可。DrawerContent 掛載後才建立 portal,伺服器端輸出為空,不會有 hydration 不一致的問題。
怎麼改面板寬度或高度?
在 DrawerContent 傳 className。左右方向預設 w-80 max-w-[85vw],上下方向預設 max-h-[85vh],用 Tailwind class 覆寫即可,例如 className="w-96"。
拖多遠才會關閉?
沿關閉方向位移超過 80px,或放開時速度超過 500px/s 就會關閉;未達門檻會彈回原位。系統開啟「減少動態效果」時,拖曳會停用,改成淡入淡出。
鍵盤與螢幕閱讀器的支援到什麼程度?
面板帶 role="dialog" 與 aria-modal="true",開啟時焦點移入、關閉後回到觸發鈕,Esc 可關閉。元件沒有內建 focus trap,面板內互動元素多時建議自行搭配。
需要哪些依賴?
motion、lucide-react、clsx 與 tailwind-merge。不用 CLI 的話,安裝區塊的「手動」分頁有原始碼取得方式。