视口感知下拉菜单

展开式操作面板或弹出式按钮组是一种实用的模式,可让用户访问更多功能,同时占用最少的空间。这种叠加模式会增加布局的复杂性,因为面板必须始终与触发元素保持关联,同时还要适应视口限制。过去,这需要复杂的 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,则需要手动后备方案。

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;
}