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