Popover vs Dialog vs Modal:搞懂原生 Web 彈窗的正確選型

從 HTML 語意、Top Layer 到互動狀態,釐清三者的關係與實務應用

Leo Chiu · · 8 min

Popover 跟 dialog 很容易分不清楚,有些 design system 甚至會用 modal 這個單字。

有用過 Bootstrap、Ant Design 的人都會覺得「彈窗」就是「modal」。

這也是 Web 常被討論的題目,Hidde de Vries(曾在 W3C 的無障礙小組工作,以及前 Mozilla 成員)就為它寫過文章跟演講。

上一篇分析 GitHub 時兩者都有出現,這篇想把 popover 跟 dialog 的差別整理一下。

拆解 GitHub 前端:Popover API、Anchor Positioning 能取代套件嗎?6 Oct 2026 · 6 min

Popover 和 dialog 差在哪?

先從使用者的角度來看 popover 跟 dialog :

dialog 比較像「必須回應的對話框」,popover 則是「看了就走的輔助資訊」。

Dialog:介面在跟使用者對話

Dialog 是介面停下來問使用者一件事,或請他完成一個小任務:

  • 「確定要刪除這個專案嗎?」
  • 「請先登入」
  • 「填寫寄送地址」
這個部落格的編輯器 publish dialog

就像店員走過來問你問題,你得回答,或至少明確說「不用了」。

HTML 規範對它的定義也是這樣:一個讓使用者完成任務、提供資訊的小視窗。規範還特別提到,右鍵選單、tooltip、下拉清單都不算 dialog。

至於會不會把背景蓋住、鎖住焦點,取決於它是不是 modal。

modal 不是另一種元件,而是 dialog 的一種狀態:

  • Modal:店員擋在門口,回答完才能繼續逛
  • 非 modal:店員遞給你一張單子,你可以邊填邊逛

Popover:臨時冒出來的東西

Popover 是平常看不到,需要時才冒出來,疊在畫面最上面,不需要了就消失:

  • 「點按鈕展開的下拉選單」
  • 「滑鼠移上去出現的說明」
  • 「點日期欄位跳出的月曆」
  • 「右下角的通知」
部落格的文章預覽 popover

比較像便利貼或對話泡泡,元素會貼在旁邊,看完或點別的地方就自己消失。使用者可以完全不理它,頁面照常操作。

Modal、dialog、popover 的關係

UX 研究機構 Nielsen Norman Group(NN/g)的〈Modal & Nonmodal Dialogs〉這樣分 dialog 的 modal 跟 nonmodal:

dialog 就像系統發出的詢問。如果是 modal 狀態,會強制你先處理完,nonmodal 則可以先晾在旁邊,讓你繼續做手邊的事。

所以可以這樣總結成一張表格:

擋住頁面不擋頁面
使用者要回應Modal dialog非 modal 的 dialog
使用者不用回應很少用Popover
  • Modal dialog:刪除確認、登入
  • 非 modal 的 dialog:Gmail 的寫信視窗
  • Popover:選單、tooltip、月曆、toast
  • 很少用:儲存或付款處理時蓋住整個畫面的 loading 遮罩

「使用者不用回應,但是擋住頁面」這個組合的元件很少用,因為把使用者擋住,又不讓他做任何事,只有很短的時間能被接受。舉例來說,像是 loading 遮罩是為了防止重複送出,處理完就該馬上消失。

「使用者要回應,但是擋住頁面」的 modal 也不是越多越好,NN/g 建議 modal 只用在必要的時候,例如會造成不可逆後果的操作、或流程繼續下去非填不可的資訊。跟使用者當下在做的事無關的內容,像一進網站就跳出來的電子報訂閱框,會消耗使用者的信任。

不過 Substack 這類平台偏偏就是這樣做,一進去文章就先跳出訂閱框,要按「No thanks」才能繼續讀。

我猜是因為轉換率,雖然要多一個步驟才能看到文章,換到的訂閱可能比把按鈕放在文末還多。

Design System 的 Popover 不等於瀏覽器的 popover

很多 Design System 裡的「Popover」是一種特定元件,點按鈕後,旁邊出現「一張帶小箭頭的卡片」。

瀏覽器的 popover 範圍大很多,選單、tooltip、月曆、toast 都算。

所以設計稿上標著 Popover 的元件,不一定就該用瀏覽器的 popover 做。


dialog 跟 popover 的 HTML 怎麼寫

回到 HTML,<dialog> 是一個元素,popover 是一個屬性。

<dialog> 自帶 dialog 的 role,瀏覽器知道它是對話框。popover 則只加行為不加語意,可以掛在任何元素上,做成選單就自己補 role="menu",做成 tooltip 就補 role="tooltip"。

<!-- 元素:本身就是對話框 -->
<dialog id="confirm">確定要刪除嗎?</dialog>
 
<!-- 屬性:掛在什麼元素上都可以 -->
<ul popover id="menu" role="menu">…</ul>

兩者的行為差異整理如下:

<dialog> + showModal()popover
擋住頁面(modal)會永遠不會
進 top layer會會
點 overlay 關閉預設不會,closedby="any" 才會auto 預設會
Esc 關閉會auto 會,manual 不會
焦點自動移進去,困在裡面不會移進去,關閉時回到按鈕
內建 roledialog沒有,要自己補
背景遮罩::backdrop有 ::backdrop,但通常不用

背景變暗,不等於 modal

modal 的「擋住頁面」,在程式上是指頁面其他部分都變成 inert,點不到、Tab 不到,螢幕閱讀器也讀不到。

Hidde de Vries 提醒,自己刻的時候只加遮罩或只做 focus trap 都不夠,showModal() 會一次處理好,還會隱含 aria-modal="true"。

popover 則永遠不是 modal,就算寫成 <dialog popover> 也一樣。

打開後點點看「背景的按鈕」:

Demo
被點了 0 次

modal dialog

試著點背景的按鈕,或按 Tab。

popover

試著點背景的按鈕,或按 Tab。

用 showModal() 打開時,背景按鈕點不到。用 popover 打開時,點背景會關掉 popover。


不寫 JS 怎麼打開 dialog?用 Invoker Commands

以前 popover 可以只用 HTML 就可以宣告,但是 dialog 卻得用 JS 呼叫 showModal()。

現在 dialog 也不用寫 JS 了。

popover 一開始的 popovertarget 只能控制 popover。後來加入的 Invoker Commands 用 commandfor 指定目標、command 指定動作,dialog 和 popover 都能這樣用:

<button commandfor="confirm" command="show-modal">刪除</button>
 
<dialog id="confirm">
  <p>確定要刪除嗎?</p>
  <button commandfor="confirm" command="close">取消</button>
</dialog>
 
<button commandfor="menu" command="toggle-popover">選單</button>
<ul id="menu" popover role="menu">…</ul>
目標可用的 command
<dialog>show-modal、close、request-close
popovershow-popover、hide-popover、toggle-popover
自訂以 -- 開頭,例如 --rotate,在目標上監聽 command 事件

怎麼關閉:light dismiss、Esc 和 close request

auto popover 點外面或按 Esc 就會關,modal dialog 預設只認 Esc 跟 close(),點背景要另外設定。dialog 被要求關閉時會先觸發 cancel,「還沒儲存」的提醒可以寫在這裡。

popover 的 auto 和 manual

popover 不寫值就是 auto,點外面或按 Esc 會自動關閉(light dismiss),而且同一時間只會開一組,開新的會把舊的關掉。

popover="manual" 則完全不會自己關,也不會關掉別人,適合 toast 這種要並存、要自己控制時間的提示。

Demo

auto

我是選單 A
我是選單 B

打開 A 再打開 B,A 會自己關掉。點空白處或按 Esc 也會關。

manual

多按幾次,通知會疊在畫面右下角。點空白處、按 Esc 都不會關,只有 × 會。

dialog 的 closedby

modal dialog 預設只能用 Esc 或 close() 關閉,點背景沒有反應。要點背景就關掉 dialog,以前得自己判斷點擊座標是不是落在 dialog 外面。

closedby 屬性把這件事變成宣告式:

值Esc點背景close()
any✅✅✅
closerequest(modal 預設)✅❌✅
none(show() 預設)❌❌✅

不過截至 2026 年 10 月,closedby Safari 還沒支援,目前只在 Technology Preview。要讓 Safari 也能點背景關閉,還是得自己補:

dialog.addEventListener("click", (e) => {
  // 點在 ::backdrop 上時,event.target 就是 dialog 本身
  if (e.target === dialog) dialog.requestClose();
});

dialog 的 requestClose() 和 close() 差在哪?

這兩個都會關掉 dialog,差別在 requestClose() 會先觸發 cancel 事件,讓你有機會用 preventDefault() 擋下來。

dialog.addEventListener("cancel", (e) => {
  if (hasUnsavedChanges()) e.preventDefault();
});
 
dialog.requestClose(); // 會先問 cancel
dialog.close(); // 直接關,不問

按 Esc、點背景(closedby="any")、Android 的返回鍵,瀏覽器都把它們當成同一種「close request」,走的就是 requestClose() 這條路。所以「還沒儲存要提醒」的邏輯寫在 cancel 裡,所有關閉方式都會被攔到。

打開下面的留言框打幾個字,再試試按 Esc、點背景、按「取消」:

Demo
打幾個字,再試著用各種方式關掉它。
  1. 事件紀錄會出現在這裡

留言

第一次關閉會被擋下來,第二次才放行,「放棄」按鈕用的是 command="close",所以完全不會觸發 cancel。

要注意的是,瀏覽器為了防止網頁困住使用者,Esc 觸發的 cancel 只有在使用者和頁面互動過之後才擋得住。連按兩次 Esc,中間沒有點擊或打字,第二次就一定會關;用程式打開 dialog、使用者什麼都還沒做就按 Esc,第一次也擋不住。所以 cancel 適合拿來提醒,不能拿來強迫使用者留下。


Popover 跟 dialog 的樣式與動畫

兩者共用的樣式工具:

  • ::backdrop:背景遮罩,dialog 和 popover 都有
  • :popover-open:popover 打開時的狀態
  • :modal:用 showModal() 打開的 dialog
  • :open:2026 年 5 月成為 Baseline,dialog、<details>、<select> 打開時都會符合

進場動畫用 @starting-style 搭配 transition-behavior: allow-discrete,寫法在上一篇有完整說明,這裡不重複。

退場動畫比較麻煩。元素關閉時會馬上離開 top layer,要等動畫跑完才離開,得在 transition 裡加上 overlay:

dialog,
[popover] {
  transition:
    opacity 150ms,
    display 150ms allow-discrete,
    overlay 150ms allow-discrete;
}

截至 2026 年 10 月,overlay 只有 Chromium 支援。Firefox 和 Safari 還是能淡出,只是淡出期間元素已經不在 top layer,如果被其他元素蓋住或被 overflow 裁掉,最後那幾毫秒會看得出來。大部分情況影響不大,寫上去當漸進增強就好。


tooltip 要用哪個?popover="hint" 和 interestfor

tooltip 可以用 popover="hint" 搭配 interestfor,hover、鍵盤聚焦都能打開,不用寫 JS,只是兩個 API 的支援度都還不夠。

上一篇提到,GitHub 的 tooltip 雖然用了 popover,但 hover 時還是要自己寫 JS 呼叫 showPopover(),因為 popovertarget 只認點擊。

拆解 GitHub 前端:Popover API、Anchor Positioning 能取代套件嗎?6 Oct 2026 · 6 min

這個缺口有兩個新 API 在補:

  • interestfor:使用者對按鈕「表示興趣」時打開目標,包含滑鼠 hover、鍵盤聚焦、觸控長按,延遲時間用 CSS 的 interest-delay 調整
  • popover="hint":專門給 tooltip 用的第三種 popover,打開時不會把已經開著的 auto popover 關掉,所以選單開著時 hover 其他按鈕,選單不會消失
<button interestfor="tip">儲存</button>
<div id="tip" popover="hint" role="tooltip">Ctrl + S</div>

截至 2026 年 10 月,interestfor 只有 Chrome 142 以上支援,popover="hint" 在 Chrome 151、Firefox 153 才換成新版規格,Safari 還在 Technology Preview。現階段可以當漸進增強,或搭配 polyfill。


混用 dialog 和 popover 的坑

混用時的問題幾乎都跟 top layer 和 inert 有關。自製的 div dialog 不在 top layer,會被 popover 蓋住;原生 modal 打開時,放在 dialog 外面的 popover 可能看得到卻點不到。

popover 蓋住自製的 div dialog

popover 會進 top layer,自製的 <div role="dialog"> 不會,所以 popover 可能蓋在對話框上面,擋住裡面的按鈕。Adrian Roselli 2023 年的建議是,先把 modal 換成原生 <dialog>,再導入 popover。

我在 Chrome 152 實測:

  • 呼叫 showModal() 時,開著的 auto popover 會自動關掉
  • manual popover 不會被關掉,但它不在 dialog 裡面,所以會跟著頁面一起變成 inert
  • 之後再打開一個放在 dialog 外面的 manual popover(例如 toast),它會顯示在 dialog 上面,但焦點進不去、按鈕也點不到

最後一點最容易踩到:toast 看得到卻按不到。要在 modal 裡用的 popover,記得放在 <dialog> 的 DOM 裡面。

Roselli 2025 年 12 月補充,這些 modal、inert 和 top layer 之間的互動,WHATWG 還在討論,未來行為可能會變。


小結

以前開發彈窗最煩的是處理 backdrop、focus trap 和 top layer,現在瀏覽器原生都幫我們處理好了。

簡單來說,選用邏輯可以直接記這三點:

  • 需要使用者強制回應(刪除確認、登入):<dialog> + showModal()
  • 隨時可被忽視的輕量互動(下拉選單、Tooltip、Toast):Popover API
  • 想告別 JS 事件監聽:直接上 Invoker Commands(command / commandfor)

目前大部分新特性都已跨瀏覽器支援,只有像 closedby 或 popover="hint" 這種延伸 API 還在進展中。現階段建議把原生 <dialog> 和 Popover 當作預設基礎,非 Baseline 的新功能當漸進增強來用即可。


Reference

每兩週,分享寫作與所見。每天進步一點點,一起在終點遇見更好的自己。

受 Cloudflare Turnstile 保護 · 隱私說明

Leo Chiu 的照片

Leo ChiuSenior Software Engineer @ KKday

資深軟體工程師,任職於 KKday Growth Team,主導網站 SEO / AEO(AI 搜尋優化)策略落地。平常在團隊裡推動 AI agent 的開發流程與效能優化。

Read more