配了三年弹层定位,每次箭头方向都要靠 JS 算——今天 CSS 自己会认位置了,@container anchored(fallback) 把这件事彻底变了

弹层跟着锚点走,这是现代浏览器最顺手的事。@position-try 给你一套备选方案:放不下就换个方向,浏览器自己会判断。但这件事有一个没填的坑——CSS 可以移动元素,但不知道它最终落在哪里。箭头该指上还是指下?不知道。渐变该往哪边晕开?不知道。动画该从哪个方向进来?还是不知道。你只能写 JS,去读元素的位置,再手动改样式。

这个坑,今天被 CSS Anchor Positioning Level 2 的 @container anchored(fallback) 填上了。

发生了什么

CSS 锚点定位 API(Anchor Positioning)在 Chrome 125 稳定之后,解决了弹层「绑在谁身上」的问题。你用 anchor-name 给锚点起名字,用 position-anchor 告诉弹层绑哪个,用 position-area 说想放哪个方位。这套语法比 Floating UI 简洁得多,浏览器自己算坐标,不用你写 Measure 代码。

然后你加了 position-try-fallbacks,给浏览器一套备选名单:如果 top 放不下,就试试 bottom;还放不下就试试 flip-block。浏览器会在运行时决定用哪个。

问题就在这里。CSS 能移动元素,但它不告诉你用了哪个备选。这意味着弹层的样式只能写死——箭头永远指上,背景渐变永远从顶部开始。你想根据实际位置调整这些?对不起,CSS 不知道。

@container anchored(fallback) 怎么填这个坑

Level 2 规格给锚定元素加了一个特殊的容器类型:container-type: anchored。

把这个属性扔到弹层上,它就变成了一个「感知自己位置的查询容器」。然后你用 @container at-rule 里的 anchored() 查询函数,配上 fallback 描述符,就能问:「现在用的是哪个备选位置?」

举一个官方的 tooltip 例子你就明白了:

/* 锚点元素 */
.anchor {
  anchor-name: --my-anchor;
}

/* 弹层(tooltip)*/
.tooltip {
  position: fixed;
  position-anchor: --my-anchor;
  /* 优先放底部 */
  position-area: top;
  /* 底部放不下就翻到对面 */
  position-try-fallbacks: flip-block;
  /* 把自己变成锚定查询容器 */
  container-type: anchored;
}

/* 默认箭头:指向下方(弹层在锚点上方) */
.tooltip::before {
  content: '▲';
  position: absolute;
  bottom: 100%; /* 弹层上方 */
}

/* 当 flip-block 备选被触发时(弹层翻到了锚点下方) */
@container anchored(fallback: flip-block) {
  .tooltip::before {
    content: '▼'; /* 箭头反过来指 */
    bottom: auto;
    top: 100%; /* 移到弹层下方 */
  }
}

position-area: top 的意思是「弹层放在锚点的顶部区域」——也就是锚点的上方。但页面滚动后弹层被顶边截断了,浏览器自动切到 flip-block 备选,弹层翻到了锚点下方。

以前这一步发生后,箭头还指上,指向一个空白的地方。现在 @container anchored(fallback: flip-block) 问到了「现在在用 flip-block」,箭头自动翻过来指。

整个过程没有 JS、没有 MutationObserver、没有 window.requestAnimationFrame 轮询。纯 CSS,浏览器原生。

不只是箭头

知道哪个备选在用,实用场景远不止调箭头方向。

渐变背景方向跟着位置走。 弹层在上方时,从上往下晕开的渐变看起来对;弹层翻到下方后,渐变方向也要反过来。用 anchored(fallback) 查一次,换一个 background-direction,一行 CSS 搞定。

入场动画方向要对应。 弹层从上往下掉,就从上面滑进来;翻到下面之后,入场方向要变成从下往上。如果动画是 CSS @keyframes 写的,只需要换 animation-name 或者调整 keyframe 里的 from 位置就行。

callout 尾巴的指向。 很多设计系统里的气泡框带小三角尾巴(callout tail),它的位置和方向完全依赖弹层和锚点的相对关系。以前靠 JS 计算 DOM 位置再写死 class;现在 container-type: anchored 加 @container anchored() 全搞定。

多级联动的嵌套弹层。 如果你的弹层里还有子弹层,子弹层也有自己的 fallback 逻辑,每一层的箭头都可以根据自己所在的 position-area 独立调整。

浏览器支持现状

Chrome 143 开始支持,Chromium 内核浏览器(Chrome、Edge)可用。Firefox 和 Safari 尚未表态。这是 Level 2 规格,仍在 Proposed 阶段,生产使用记得加 @supports 渐进增强:

/* 渐进增强:没有 anchored 查询时,至少保证位置对 */
.tooltip {
  /* 基础样式 */
}

@supports (container-type: anchored) {
  .tooltip {
    container-type: anchored;
  }
  /* @container anchored() 样式放这里 */
}

在 Firefox 和 Safari 里,@position-try 本身是支持的(Chrome 125+ Safari 17.2+),弹层位置会正确切换,只是箭头等从属样式不会自动适配。这是合理的降级——功能不废,只是少了一层上下文感知。

对比以前怎么做的

没有这个 API 之前,你需要:

  1. 在滚动和 resize 事件里调用 getBoundingClientRect(),拿到弹层和锚点的实时坐标
  2. 比较坐标,判断弹层在锚点的哪一侧
  3. 根据结果 addClass / removeClass,改箭头方向
  4. 记得在 cleanup 时 removeEventListener,否则内存泄漏
  5. 如果弹层里有嵌套子元素,子元素的箭头也要重复这套逻辑

这套代码大概要 40-60 行 JS。现在 10 行 CSS,而且运行在浏览er’s layout engine 里,比 JS 算的更准确、没有并发问题、也不依赖 React/Vue 的状态管理。

可以动手了

如果你的项目里有 tooltip、dropdown、popover 这类弹层组件,且已经在用 @position-try 做自适应定位,这个 API 是直接的增强。在 .tooltip 上加 container-type: anchored,再加几行 @container anchored(fallback) 的样式,箭头、尾巴、渐变就能跟着位置自动适配了。

如果你是设计系统作者,这个 API 让「上下文感知的组件样式」第一次成为纯 CSS 能力。以前组件想知道自己被摆在哪里,只能靠 JS 往 DOM 上写 data-* 属性;现在靠 CSS 自己读。这是 CSS 这几年最接近「组件自感知」的一次。

Level 2 规格还在发展中,Firefox/Safari 跟进之后会成 Baseline 能力。在那之前,用 @supports 渐进增强保护好你的默认样式,然后让 Chromium 用户先用上。

评论区

0 条评论

登录后可评论。