覆蓋層與回饋

Drawer

從畫面邊緣滑入的抽屜面板,支援四向、遮罩點擊關閉,並可單軸拖曳滑走關閉。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add drawer

還沒有專案,或安裝遇到問題?看快速開始 →

Drawer 是用 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型別預設值說明
childrenReact.ReactNode—內容,需包含 DrawerTrigger 與 DrawerContent
openboolean—受控模式的開啟狀態,不傳則由元件內部管理
onOpenChange(open: boolean) => void—開啟狀態變更時的回呼(受控與非受控皆會觸發)

DrawerTrigger

Prop型別預設值說明
childrenReact.ReactNode—觸發鈕內容
classNamestring—追加到觸發鈕的 className

DrawerContent

Prop型別預設值說明
side"top" | "right" | "bottom" | "left""right"抽屜滑入的方向
classNamestring—追加到面板容器的 className
childrenReact.ReactNode—面板內容

DrawerClose

Prop型別預設值說明
childrenReact.ReactNodelucide-react X 圖示自訂按鈕內容
classNamestring—追加到關閉按鈕的 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 的話,安裝區塊的「手動」分頁有原始碼取得方式。