可視區域感知下拉式選單

網頁功能: 錨定位置 彈出式視窗

展開式動作面板或彈出式按鈕群組,可讓使用者存取其他功能,同時盡可能減少佔用空間。這種疊加模式的版面配置較為複雜,因為面板必須與觸發元素保持連結,同時適應可視區域限制。以往,這需要複雜的 JavaScript 程式庫 (例如 Popper.js 或 Floating UI) 來計算位置及處理碰撞。

CSS 錨定定位提供宣告式、效能最佳化的方式,可完全在 CSS 中處理這些關係,讓瀏覽器以原生方式管理定位和溢位邏輯。

1. 定義按鈕和面板的關係

第一步是建立觸發按鈕,使用 Popover API 開啟疊加層容器。

無障礙功能重要區別:這個模式明確模擬彈出式視窗中顯示的按鈕群組,而非真正的 ARIA 選單。除非您已完整實作對應的鍵盤導覽合約 (例如處理項目之間的空間方向鍵導覽),否則請勿套用 role="menu" 或 role="menuitem"。基於相同原因,請勿在觸發條件中加入 aria-haspopup,因為其值 (menu、listbox、tree、grid 或舊版 true) 承諾目標會公開相符的角色,但這個模式不會。

<button popovertarget="action-panel">
  Open Actions
</button>
<!-- Use the Popover API (`popover="auto"`) for the overlay to ensure it is placed in the top layer and handled accessibly by the browser. -->
<div id="action-panel" popover="auto" class="panel">
  <button class="action-item" type="button">Edit</button>
</div>

這會在按鈕和面板之間建立「隱含」錨定關聯,讓面板能以原生方式相對於按鈕定位,不需要在按鈕上使用 anchor-name,或在面板上使用 position-anchor。

2. 使用 position-area 定位

請使用 position-area,將目標放置在相對於錨點的 3x3 格線,而不是手動設定 top/left 偏移。

.panel {
  /* 
     Position the panel below the anchor (block-end), 
     aligned to the start of the anchor and spanning to its end (span-inline-end).
  */
  position-area: block-end span-inline-end;
  
  /* Reset insets to allow the grid to take control */
  inset: auto;
}

建議使用邏輯關鍵字 (span-inline-end、block-start),而非實體關鍵字 (left、top),以便自動支援 RTL 和不同的書寫模式。

強制:請勿在 position-area 中混用實體和邏輯關鍵字。

3. 實作邊緣韌性 (備用機制)

為避免面板在螢幕邊緣遭到截斷,請定義瀏覽器在預設位置溢位時應嘗試的「嘗試策略」。

.panel {
  /* 
     If the panel overflows the bottom, flip it to the top (flip-block).
     If it overflows the inline edges, flip it horizontally (flip-inline).
     If it overflows both (e.g., in a corner), flip on both axes (flip-block flip-inline).
  */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}

如果下拉式選單或彈出式選單包含方向箭頭或邊框,且需要根據所選的備用位置變更樣式,請參閱「位置感知工具提示」。如要瞭解如何在不使用 JavaScript 的情況下,以宣告方式叫用對話方塊和彈出式視窗,請參閱「宣告式對話方塊和彈出式視窗控制項」。

備用策略

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 預設定位。

如要支援不支援錨定定位的瀏覽器,您必須設定合理的位置。根據預設,彈出式視窗會置於畫面中央,這可能適用於您的用途。

在某些情況下,您可以使用 @oddbird/css-anchor-positioning polyfill,為部分錨定位置用途新增支援。不支援隱含錨點,因此您「必須」在觸發條件中加入錨點名稱。此外,polyfill 不支援彈出式視窗上的 position-area,因此您必須在所需插邊上使用 anchor()。

<!-- MANDATORY: Conditionally install the anchor positioning polyfill -->
<script type="module">
  if (!("anchorName" in document.documentElement.style)) {
    await import("https://unpkg.com/@oddbird/css-anchor-positioning");
  }
</script>
.panel {
  /* Mandatory: use explicit anchor name */
  position-anchor: --kebab-anchor;
  /* Mandatory: use insets rather than position-area for positioning */
  bottom: auto;
  right: auto;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
}