對話方塊

總覽

請使用原生 <dialog> 元素,建立強制回應和非強制回應對話方塊。它提供無障礙語意、焦點處理、頂層算繪和 Esc 處理,而自訂疊加層必須手動重建。

選擇疊加類型

選取第一個符合的規則。如果不確定,請使用規則 1。

規則 1:重疊廣告干擾使用者並遮蓋網頁 (預設)

適用於確認、表單、登入、破壞性動作,以及任何未另行說明的「互動視窗」、「對話方塊」、「燈箱」或「彈出式視窗」要求。

使用 <dialog> command="show-modal" 以宣告方式開啟為強制回應視窗 (請參閱宣告式對話方塊和彈出式視窗控制項),或使用 showModal() 從指令碼開啟。

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

瀏覽器提供:惰性頁面、焦點限制、頂層、::backdrop、Esc 和返回手勢關閉,以及焦點返回叫用器。

  • 請勿新增 role="dialog"、aria-modal、焦點陷阱、inert 切換或 Esc 處理常式。
  • 請勿使用 show() 或 open 屬性開啟。
  • 務必使用下列 CSS 停止捲動背景頁面。詳情請參閱「防止強制回應對話方塊下方的頁面捲動」。
  • 請勿在 html 或 body 上使用 overflow: hidden 或 position: fixed 鎖定捲動功能。
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

不適用於選單或工具提示:請參閱規則 3。

規則 2:面板開啟時,使用者仍可在頁面中繼續操作

用於尋找列、檢查器、調色盤和撰寫窗格,這些在語意上仍屬於對話方塊。

使用 <dialog popover> 建立頂層面板,並設定彈出式視窗關閉行為 (auto 會輕觸關閉,manual 則不會)。只有在面板不應位於頂層時,才使用 dialog.show()。

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

頁面會保持互動狀態,不會變成無效。

  • 務必加入清楚可見的關閉按鈕。
  • 請勿使用 showModal(),否則會封鎖網頁。

不適用於封鎖確認:請參閱規則 1。

規則 3:疊加層不是對話方塊

適用於選單、下拉式選單、工具提示、懸浮資訊卡和浮動式訊息。

請使用含有 popover 屬性的語意元素,而非 <dialog>。

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

選擇關閉方式

預設值取決於對話方塊的開啟方式。如果 closedby 遺失或無效,showModal() 的行為會與 closerequest 相同,而任何其他開啟方式的行為會與 none 相同。

  • 僅限 Esc 鍵和返回手勢 (互動視窗預設值):不設定任何值,或設定 closedby="closerequest"。
  • 點選對話方塊外側也可關閉:使用 closedby="any"。
  • 只有開發人員可以關閉 (例如含有未儲存變更的表單):使用 closedby="none" 並新增確認路徑。請參閱下方的「結語」。

規範

無障礙元素名稱和初始焦點

  • 務必使用 aria-labelledby 指向標題,為每個對話方塊命名,或在沒有可見標題時使用 aria-label。
  • 務必將 autofocus 放在主要控制項或第一個欄位。
  • 務必在破壞性確認中,將 autofocus 放在安全動作 (例如「取消」或「保留」) 上。
  • 請勿編寫程式碼,在關閉時傳回焦點。瀏覽器會將其傳回給叫用者。

開場白

Closing

  • 務必使用 <form method="dialog"> 按鈕或 command="close" 關閉。
  • 務必謹慎選擇 closedby:any (輕觸關閉和 Esc)、closerequest (僅限 Esc,模態預設值) 或 none (僅限開發人員控制的關閉)。
  • 建議使用 command="request-close" 或 requestClose() 建立「取消」按鈕,以遵循未儲存變更邏輯。系統會先觸發 cancel,並忽略 closedby。
  • 務必將 closedby 視為漸進增強。Esc 和行動裝置返回手勢已可關閉互動視窗,不需使用這個屬性。

連結至關閉

  • 務必在 close 監聽器中讀取 dialog.returnValue,瞭解使用的是哪個按鈕,然後使用 dialog.returnValue = '' 重設。Esc 和其他關閉要求不會設定任何值,且瀏覽器是否會清除先前的值也各有不同,因此如果不重設,舊值可能會外洩。將 '' 視為「未選擇即關閉」。
  • 建議使用 cancel 攔截 Esc 和 request-close,但請勿在使用者未啟動的情況下,依賴 preventDefault() 重複按下 Esc。
  • 務必使用 closedby="none",並針對未儲存的表單加入明確的確認步驟。
  • 務必在 cancel 遭到禁止時,告知使用者對話方塊保持開啟的原因:顯示可見訊息,並從接收焦點的控制項 (例如「捨棄」) 使用 aria-describedby 參照該訊息,以便螢幕閱讀器在焦點移動時朗讀該訊息。如果沒有這個屬性,焦點移到新按鈕時,系統只會讀出該按鈕的名稱。
  • 請勿讓確認動作移動游標下方的控制項。保留訊息空間 (使用 visibility 隱藏,而非 display 或 hidden),並在現有按鈕不會移動的位置 (例如靠右對齊列的第一個按鈕) 新增破壞性按鈕,這樣一來,在同一位置點按第二次就不會捨棄使用者的變更。

非強制回應對話方塊

  • 建議使用 <dialog popover> (auto 或 manual) 做為頂層面板,並在不需要頂層時使用 dialog.show()。
  • 務必在每個非強制回應對話方塊中,加入可見的關閉按鈕。

背景和造型

  • 務必設定 ::backdrop 的樣式,並使用 dialog:modal 指定僅限互動視窗的樣式。
  • 務必將動畫留給「在頂層之間建立元素動畫」,將拖曳留給「建構可拖曳元素」,並將重新設定父項留給「永久頂層 UI」。
  • 請勿無條件在 dialog 上設定 display。會覆寫關閉狀態。使用 dialog[open] 設定範圍。

無障礙設定

  • 請勿在原生互動視窗中加入 role="dialog"、aria-modal、手動焦點陷阱或 inert 切換功能。
  • 建議使用原生 <button>,並為關閉控制項設定無障礙元素名稱。
  • 建議:對話方塊標題一律使用相同的標題層級。

備用策略

closedby (dialog-closedby)

如果沒有 closedby,互動視窗仍會透過 Esc 和返回手勢關閉,只有點選外部的功能會失效,因此這是優雅降級。系統會以相同方式忽略 closedby="none":Esc 會觸發 cancel,然後關閉對話方塊。請將未儲存變更防護機制保留在 cancel 處理常式中,而非僅保留在 closedby="none" 中,這樣一來,即使缺少 closedby,也能受到保護。如果基準目標需要輕觸關閉,請在缺少 closedby 的地方新增:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

將捲動限制視為漸進增強。請勿偵測功能:即使不可捲動的容器忽略該屬性,@supports 也會回報支援該屬性。如果未強制執行,對話方塊仍會在內容溢位時捲動,且 showModal() 仍會使頁面處於閒置狀態。請勿在 html 或 body 上改用 overflow: hidden 或 position: fixed,否則會移除根捲軸並移動版面配置。

叫用器指令 (command/commandfor)

如果 Baseline 目標不包含 Invoker 指令,請保留相同的標記,並視情況載入 polyfill (與漸進增強功能相同的體驗)。偵測介面,絕不無條件載入:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

只有規則 2 和 3 使用彈出式視窗。

Popover 的備用方案和瀏覽器支援

Popover API 主要是漸進增強,但其定義行為 (頂層升級、輕觸關閉和 popovertarget 叫用) 沒有僅限 CSS 的對等項目。舊版瀏覽器需要 polyfill,如果不想提供,則需要手動備用方案。

Polyfill:如要在舊版瀏覽器中支援 popover 屬性,請視情況載入 @oddbird/popover-polyfill。必要:檢查 HTMLElement.prototype 上的 popover 屬性,偵測功能,並僅在缺少原生支援時載入 polyfill,請勿無條件載入。

使用套件組合器或匯入對應:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

如果沒有繫結器,請從 <script type="module"> 內的 CDN 匯入:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

樣式注意事項:Polyfill 無法定義實際的 :popover-open 虛擬類別,因此會改為套用 .\:popover-open 類別。必要:請使用 :is() 或 :where() 結合兩者,否則不支援 :popover-open 的瀏覽器會捨棄整個規則:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

或者,如要使用不含 polyfill 的舊版備用方案,請使用 position: fixed,並透過 getBoundingClientRect() 手動計算座標,或在用途允許的情況下,使用 inset: auto 預設定位。