覆蓋層與回饋
錨定浮層
按鈕觸發的錨定浮層:依 side/align 定位並在視窗邊緣自動翻轉,淡入、微縮放、沿方向滑入,焦點進出與點外關閉完整處理。
由按鈕觸發、貼著觸發鈕出現的浮層,適合放篩選條件、日期區間、快速設定這類「不值得開一個對話框」的小表單。以複合元件組成:Popover.Trigger、Popover.Content、Popover.Close、Popover.Arrow。浮層依 side/align 定位,碰到視窗邊緣會翻到對側並沿交叉軸推回視窗內;進場是淡入加 0.95 微縮放、沿方向滑入 8px,縮放原點跟著實際落點。開啟時焦點移進浮層、關閉時回到觸發鈕,點外與 Escape 都能關閉。定位邏輯自帶在單一檔案內,不依賴任何定位套件。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add popoverPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
100
4
編輯中的呼叫預設屬性
<Popover/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
安裝
npx webberui@latest add https://webberui.com/r/popover.json或在 components.json 設定 registries 後,改用 @webberui/popover 安裝。
使用
import { Popover } from "@/components/ui/popover";
<Popover>
<Popover.Trigger>設定期間</Popover.Trigger>
<Popover.Content side="bottom" align="start" aria-labelledby="range-title">
<Popover.Arrow />
<p id="range-title" className="text-sm font-medium">報表期間</p>
{/* 你的表單內容 */}
<Popover.Close>完成</Popover.Close>
</Popover.Content>
</Popover>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
open | boolean | — | 受控的開啟狀態;未提供時由元件內部管理(非受控) |
defaultOpen | boolean | false | 非受控模式的初始開啟狀態 |
onOpenChange | (open: boolean) => void | — | 開啟狀態變動時回呼 |
modal | boolean | false | 模態模式:Tab 焦點鎖在浮層內、鎖住頁面捲動、後方加透明點擊攔截層 |
children | ReactNode | — | Popover.Trigger 與 Popover.Content 等子元件 |
Popover.Trigger
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
asChild | boolean | false | 為 true 時不渲染按鈕,改把 ref、點擊與 aria 屬性合進唯一的子元素 |
children | ReactNode | — | 觸發鈕內容 |
className | string | — | 透傳到觸發鈕,以 cn() 併入預設樣式 |
其餘 button 屬性(aria-label、disabled、onClick…)會透傳到按鈕上。
Popover.Content
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "bottom" | 浮層相對觸發鈕的方向;空間不足時會自動翻轉到對側 |
align | "start" | "center" | "end" | "center" | 交叉軸對齊方式 |
sideOffset | number | 4 | 與觸發鈕之間的距離(px) |
alignOffset | number | 0 | 交叉軸上的位移(px),正值往內縮 |
collisionPadding | number | 8 | 碰撞偵測時與視窗邊緣保留的距離(px) |
avoidCollisions | boolean | true | 是否在超出視窗時翻轉方向並沿交叉軸推回視窗內 |
duration | number | 100 | 進出場動畫時長(毫秒),同時寫入 --wb-popover-duration |
onEscapeKeyDown | (event: KeyboardEvent) => void | — | 按下 Escape 時回呼;呼叫 event.preventDefault() 可阻止關閉 |
onInteractOutside | (event: PointerEvent) => void | — | 在浮層與觸發鈕之外按下指標時回呼;呼叫 event.preventDefault() 可阻止關閉 |
aria-labelledby | string | — | 指向浮層標題元素的 id,供螢幕閱讀器朗讀對話框名稱 |
aria-label | string | — | 沒有可見標題時,直接提供對話框名稱 |
children | ReactNode | — | 浮層內容 |
className | string | — | 透傳到浮層根元素,以 cn() 併入預設樣式 |
Popover.Close
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
children | ReactNode | — | 按鈕內容;未提供時顯示 × 圖示(並帶 aria-label="關閉") |
className | string | — | 透傳到關閉鈕,以 cn() 併入預設樣式 |
Popover.Arrow
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
width | number | 10 | 箭頭底邊寬度(px) |
height | number | 5 | 箭頭高度(px) |
className | string | — | 透傳到箭頭 SVG,可用 fill-*/stroke-* 改色 |
另外匯出型別 PopoverSide 與 PopoverAlign。
細節
- 定位:浮層以 portal 掛到
document.body、position: fixed,座標取整避免文字模糊。主軸貼在觸發鈕外側sideOffsetpx;溢出視窗且對側空間較大時翻轉,交叉軸則 clamp 回視窗內(浮層比視窗還寬時貼齊左邊)。 - 重算時機:開啟當下同步量一次,之後觀察觸發鈕與浮層的尺寸變化、視窗縮放與所有祖先容器的捲動,以
requestAnimationFrame節流成每幀最多一次;關閉時全部移除。 - 第一次量測完成前浮層保持
visibility: hidden,不會在左上角閃一下;量完才開始進場動畫。 - 浮層根元素帶
data-side與data-align(翻轉後的實際值)與data-state,方便寫依方向而異的樣式。 - 動畫節奏外露為
--wb-popover-duration:浮層內的 CSS transition 都讀這個變數,可在全域覆寫。 - 箭頭指向觸發鈕中心,並避開浮層圓角;填色與描邊和浮層同色,蓋住相接處的邊框。
- 巢狀浮層:Escape 只關閉最上層;點擊落在子浮層內不會讓父浮層關閉。
可及性
- 觸發鈕帶
aria-haspopup="dialog"、aria-expanded,開啟時以aria-controls指向浮層;浮層為role="dialog",可用aria-labelledby或aria-label命名,modal 時加上aria-modal - 開啟後焦點移到浮層內第一個可聚焦元素(沒有就聚焦浮層本身);以任何方式關閉時,若焦點仍在浮層內就回到觸發鈕
- 非 modal:Tab 越過最後一個元素會移到觸發鈕之後的元素並關閉浮層,Shift+Tab 越過第一個元素回到觸發鈕;modal:Tab/Shift+Tab 在浮層內循環
- Escape、在浮層外按下指標、
Popover.Close都會關閉浮層;所有按鈕都有 focus-visible 外框 - 使用者系統開啟「減少動態效果」時,只保留淡入淡出(時長不變),不縮放、不位移