起因
每个 React 项目都装 @floating-ui/react 或 popper.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。
踩过的坑
-
anchor-name 唯一性:同 anchor-name 多个 target 时只第一个起效。
动态生成的组件用 useId 保证唯一。 -
popover vs dialog 选错:modal 阻塞场景仍是 dialog 的设计意图;
popover 给非阻塞浮层。混用导致 UX 怪。 -
position-try-fallbacks 顺序敏感:写错顺序 fallback 选不优。
测视口各种尺寸验证。 -
animation 不工作:popover 进出动画需要
:popover-open+
@starting-style配 transition。CSS 语法新,文档要看 latest。 -
Tailwind class 没原生支持:要写自定义 utility。Tailwind v4
预计支持 anchor positioning utility。
登录后参与评论。