CSS anchor positioning:原生写 tooltip / popover(不再用 Popper.js)

起因

每个 React 项目都装 @floating-ui/reactpopper.js 解决 tooltip /
dropdown / popover 的"跟着元素,但溢出视口时自动翻转" 这类位置计算。
代码体积 + 学习曲线 + 抽象层都有成本。

Chrome 125+(2024 中)开始支持 CSS Anchor Positioning
原生 CSS 写定位浮动元素。Firefox / Safari 跟进中。

解决方案

最简单的 tooltip

<button class="btn">Hover me</button>
<div class="tooltip">I am a tooltip</div>

<style>
.btn {
  anchor-name: --my-btn;     /* 命名锚点 */
}

.tooltip {
  position: absolute;
  position-anchor: --my-btn;       /* 指定锚点 */
  position-area: top;              /* 在锚点上方 */
  margin: 4px;
}
</style>

效果:tooltip 自动定位到 btn 正上方 4px。

自动翻转避免溢出

.tooltip {
  position: absolute;
  position-anchor: --my-btn;
  position-area: top;

  /* 如果上方放不下,自动 fallback 到下方 */
  position-try-fallbacks: --bottom;
}

@position-try --bottom {
  position-area: bottom;
}

视口顶部空间不够 tooltip → 自动跑到下面。Popper.js 的 "flip middleware"
原生实现。

多 fallback

.tooltip {
  position: anchor: --my-btn;
  position-area: top;
  position-try-fallbacks:
    top right,
    bottom right,
    bottom left,
    top left;
}

按顺序尝试位置,第一个能完整显示的胜出。

dropdown menu

<button id="trigger" popovertarget="menu">Menu ↓</button>
<menu id="menu" popover>
  <li><button>编辑</button></li>
  <li><button>删除</button></li>
</menu>

<style>
#trigger { anchor-name: --trigger; }

#menu {
  position-anchor: --trigger;
  position-area: bottom span-right;
  min-width: anchor-size(width);    /* menu 至少跟 trigger 一样宽 */
  margin: 4px 0;
}
</style>

popover 是 HTML 新属性(2023+),让任意元素能 toggle 显示,
配 anchor positioning 写下拉菜单几行搞定。
完全无 JavaScript

popover API

<button popovertarget="dialog">Open</button>
<div id="dialog" popover>
  <h2>对话框</h2>
  <button popovertarget="dialog" popovertargetaction="hide">关闭</button>
</div>

popover 属性让元素自动有:

  • ESC 关闭
  • 点击外部关闭(auto 模式)
  • 出现在 top layer(覆盖任何 z-index)
  • 焦点管理
#dialog {
  /* 默认显示在浏览器中心 */
  margin: auto;
  /* 进入动画 */
  &:popover-open {
    animation: fade-in 0.2s;
  }
}

@keyframes fade-in {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

modal 风格对话框:

<dialog id="modal">
  <p>真的删除吗?</p>
  <button onclick="modal.close()">取消</button>
  <button>确认</button>
</dialog>
<button onclick="modal.showModal()">删除</button>

<dialog> 元素 + showModal() 自动焦点 trap + backdrop + ESC 关闭。
零依赖 modal。

跟 Popper.js / floating-ui 对比

CSS anchor + popover Popper.js / floating-ui
浏览器支持 Chrome/Edge 125+ / Safari/FF 跟进 全支持
bundle 影响 0 5-15 KB
灵活度 中(CSS 限制) 高(JS 任意)
实施 CSS only JS hook + ref
Modal trap popover 自动 第三方库

2025-2026 浏览器覆盖率到位后可以全切。
现在写新项目:渐进增强——支持的浏览器用 CSS,老的退化用 JS 库。

渐进增强

if (CSS.supports('position-anchor: --x')) {
  // 啥都不做,CSS 处理
} else {
  // fallback 到 floating-ui
  await import('@floating-ui/dom').then(...)
}

React 集成

只要 React 渲染对应 HTML:

function Tooltip({ children, label }) {
  return (
    <>
      <span className="tooltip-target">{children}</span>
      <span className="tooltip-content" role="tooltip">{label}</span>
    </>
  )
}
.tooltip-target {
  anchor-name: --tt;
}
.tooltip-content {
  position: absolute;
  position-anchor: --tt;
  position-area: top;
}

不需要 ref / 不需要 useState 控制位置。CSS 自动。

注意:每个 tooltip 实例需要唯一 anchor-name 否则冲突。
React 里:

const id = useId()
<span style={{ anchorName: `--tt-${id}` }}>...</span>
<span style={{ positionAnchor: `--tt-${id}` }}>...</span>

实战:context menu

右键菜单:

<div id="cm-target">右键我</div>
<menu id="ctxmenu" popover>
  <li><button>复制</button></li>
  <li><button>粘贴</button></li>
  <li><button>删除</button></li>
</menu>

<script>
const target = document.getElementById('cm-target')
const menu = document.getElementById('ctxmenu')

target.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  // 把 menu 定位到鼠标位置
  menu.style.left = `${e.clientX}px`
  menu.style.top = `${e.clientY}px`
  menu.style.position = 'fixed'
  menu.showPopover()
})
</script>

showPopover() API 触发显示。点外面 / ESC 自动关闭。

与 dialog / modal 区别

dialog (showModal) popover (showPopover)
用法 阻塞性对话框 任意浮动元素
backdrop ::backdrop 默认黑半透 无(popover=manual 时)
inert 背景内容自动 inert 看模式
ESC 自动关 自动关
嵌套 多个 dialog 栈 popover 也可栈

模态对话框用 <dialog>;非模态浮层用 popover

等待中的浏览器支持

Chrome 125+     ✅ (2024-05)
Edge 125+        ✅
Safari 17.4+      部分 (popover 完整,anchor positioning 部分)
Firefox 134+      anchor 部分

2026 末预计全支持。在那之前生产用 Popper.js 兜底;
个人项目 / Chrome 内部工具立刻可用。

浏览器特性检测

@supports (position-anchor: --x) {
  /* 新 API */
}

@supports not (position-anchor: --x) {
  /* fallback:用 absolute + JS 控制 */
}

我的实验感受

试用一周:

  • 简单 tooltip / dropdown:CSS 写 5 行 = floating-ui 50 行
  • 复杂多步定位 / 动画 / 拖拽 → 还是 JS 灵活
  • popover API 替代了 90% modal use case,不再需要 Headless UI Dialog

未来 1-2 年逐步迁移;现在新组件优先用 CSS 方案 + fallback。

踩过的坑

  1. anchor-name 唯一性:同 anchor-name 多个 target 时只第一个起效。
    动态生成的组件用 useId 保证唯一。

  2. popover vs dialog 选错:modal 阻塞场景仍是 dialog 的设计意图;
    popover 给非阻塞浮层。混用导致 UX 怪。

  3. position-try-fallbacks 顺序敏感:写错顺序 fallback 选不优。
    测视口各种尺寸验证。

  4. animation 不工作:popover 进出动画需要 :popover-open +
    @starting-style 配 transition。CSS 语法新,文档要看 latest。

  5. Tailwind class 没原生支持:要写自定义 utility。Tailwind v4
    预计支持 anchor positioning utility。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。