主題: Web platform
Popover API:少一點 UI 狀態,不是免費的無障礙
原生 Popover 能取代一部分 toggle、click-outside 與 z-index 程式碼;但它始終是 non-modal UI,不會替你決定語意、內容或產品流程。
動態迷因(展開/收合)
前端最熟悉的一小段 boilerplate,大概是這樣:一個 isOpen state、按鈕 click handler、document-level click-outside、Escape listener、再加上不斷升高的 z-index。每個都合理;累積起來,卻常只是為了顯示一段輔助說明或一個小選單。
Popover API 值得注意,不是因為它讓 popup 更酷,而是它把這種常見行為交回瀏覽器。MDN 將它列為 Baseline 2025:最新裝置與瀏覽器已可用,但舊版 client 仍可能不支援。
我的判斷原則是:
需要暫時、non-modal 的補充內容時,先用 Popover;需要使用者完成決定或表單、而且頁面其他地方不該繼續操作時,用
<dialog>。原生行為能少寫狀態,不會替你選好互動語意。
最小範例,少掉的是協調工作
一個宣告式 Popover 不需要 React state,也不需要全域 click listener:
<button popovertarget="save-help">儲存什麼?</button>
<div id="save-help" popover="auto">
<p>我們只會儲存草稿與你明確送出的資料。</p>
<button popovertarget="save-help" popovertargetaction="hide">
關閉
</button>
</div>
popover 讓元素成為 Popover;popovertarget 把按鈕和目標接起來。沒有指定 popovertargetaction 時,按鈕預設為 toggle。auto 是預設狀態,支援 light dismiss:使用者點外面或按 Escape 能離開。
瀏覽器也會把它放到 top layer,不必再用某個「一定比 navbar 大」的 z-index 常數猜誰應該蓋在誰上面。當 invoker 與 Popover 以這種方式連結,鍵盤 tab 順序與焦點回到 trigger 的基本行為也由瀏覽器處理。
這正是適合交給 platform 的範圍:
- 非破壞性的說明、teaching UI、通知摘要。
- 不必鎖住背景的 action menu 或 content picker。
- 顯示後仍允許使用者繼續閱讀或操作頁面。
但「少寫 JavaScript」不是選用 popover 的唯一理由。先問使用者可不可以忽略這塊內容;如果答案是可以,Popover 很可能對。如果不可以,它就不是 dialog 的省碼版。
Popover 與 dialog 的邊界,是 modal,不是外觀
兩者都能浮在內容上方,所以最容易被當成同一件事。差別在 interaction contract。
Popover 永遠是 non-modal:頁面其他部分仍可互動。<dialog> 用 showModal() 開啟時,瀏覽器會把其餘內容設為 inert,並提供 modal semantics。這很適合刪除確認、付款、必填流程或任何「先處理這件事才能繼續」的工作。
| 情境 | 選擇 | 原因 |
|---|---|---|
| 「這個欄位會儲存什麼?」 | Popover | 使用者可以看完繼續填表 |
| 通知/帳號的小選單 | Popover | 背景仍可操作,離開成本低 |
| 刪除專案確認 | <dialog> modal |
不該讓危險動作和背景操作同時發生 |
| 多欄位設定或支付步驟 | <dialog> modal |
需要明確焦點與完成/取消流程 |
若你真需要 dialog 的語意、但不想 block 背景,HTML 也允許 <dialog popover>。這不是理由把所有 overlay 都改成 dialog;它只是提醒我們:element semantics 和顯示/關閉行為是兩個獨立決定。
動態迷因(展開/收合)
它有 a11y 預設,不是 a11y 成品
原生 invoker 關係會建立 aria-expanded 與 aria-details 的隱含關聯,並協助焦點前進與回到 trigger。這很有價值,但它沒有回答下面幾題:
- 這是 tooltip、menu、alert 還是一段互動表單?內容與 role 仍要符合真正目的。
- 手機、鍵盤與螢幕閱讀器使用者是否都能觸發與離開?只靠 hover 的資訊通常不是好 default。
- 若內容有重要動作,是否有看得見、可點擊的關閉/取消路徑?
- trigger 的文字是否說清楚會發生什麼?「更多」通常不如「查看儲存方式」可理解。
尤其別把 title attribute 當作 tooltip 設計。它在 touch 與 keyboard 上都不可靠,內容也很難被一致地探索。若說明重要,就用真正可操作的 button 與 Popover;若不重要,通常不用另外藏一層。
Progressive enhancement 的成本,要和重要性一起算
Baseline 是很好的預設訊號,不是「所有 client 都能看到」的保證。對於非關鍵教學、可忽略的提示,直接用 Popover 往往已經是最簡單的 progressive enhancement。
若功能不能缺席,不要先寫一個 full polyfill。先保留能完成任務的 inline copy、普通頁面或 <dialog> flow,再按需要做 feature detection:
const supportsPopover = Object.hasOwn(HTMLElement.prototype, "popover");
只有真的需要兩條 interaction path 時才使用這個分支。想確認是否已開啟時,可用 :popover-open;需要在開關後同步 analytics 或其他 UI 時,再聽 toggle event。不要為了顯得「platform-native」把每個狀態又搬回一層 JavaScript state。
一個適合 code review 的檢查表
準備加新 overlay 時,先過這五題:
- 使用者能不能繼續操作背景?不能就從 modal dialog 開始想。
- 是否只是顯示/隱藏一小塊 non-critical content?是的話,原生 Popover 優先。
- 有沒有用標準 button 作為 trigger,並提供明確關閉方式?
- 支援不足的舊 client,使用者還能完成主要任務嗎?
- 是否真的需要額外 library,還是只是在重做 platform 已提供的行為?
Popover API 最好的效果不是把 UI 變得漂浮,而是把互動 contract 變得誠實。需要 background 仍可操作的,就交給 Popover;需要暫停並做決定的,就用 dialog。剩下的 state、document listener 和 z-index 競賽,能不寫就別寫。