Skip to content

浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析 #275

Description

@chokcoco

从 Chrome 125 开始,支持了一个全新的 CSS 特性 - Anchor Positioning,翻译过来即是锚点定位

在之前的文章中,我们较为系统的讲述了这个新特性的使用,感兴趣的可以翻开一下前文:抢先体验!超强大的 Anchor Positioning 锚点定位

在本文中,我们将使用锚点定位,实现一个简化版本的 Popover 功能。下面,我们将一起一探究竟。

传统 Popover 功能

长话短说,在日常的前端需求开发中,有这么一种场景,我们需要 hover 某个具体元素,以弹出一个弹出层,像是这样:

如果我们将弹出层的 DOM 结构,写在某个具体的元素内部,譬如放在与被 Hover 元素同级。如果被 Hover 元素的祖先元素的某一层设置了 overflow: hidden,则可能会出现这种截断现象:

为了避免这种情况的发生,一般情况下,常见的解决方案都是会将弹出层的 DOM,插入到页面最外层的 <body> 容器之下,再通过实时计算位置,将该弹出层定位到被 hover 元素附近。这也就是类似于 popper.jsTippy.js 等传统库所干的事情。

核心逻辑如下:

  1. 通过 JavaScript 获取触发元素的位置信息(getBoundingClientRect)
  2. 计算视口剩余空间(需考虑滚动位置、窗口尺寸等)
  3. 动态调整 Popover 位置(需处理边界碰撞、翻转逻辑)
  4. 添加 resize/scroll 事件监听器进行位置修正

其核心在于,通过 Javascript 动态计算当前 hover 元素的位置,将弹出层定位到当前 hover 元素附近合适的位置,并且处理好各种边界场景,这里很重要一点是强依赖于 Javascript 的计算。

因为传统的 CSS,是没有办法改变元素的定位父元素的能力的。

而有趣的是,全新的 CSS 特性 - Anchor Positioning 锚点定位就是为了解决这个问题的。

快速了解 Anchor Positioning 锚点定位

那,到底什么是 Anchor Positioning 锚点定位呢?我们通过一个 DEMO 快速上手。

假设我们在页面上有这么三个元素:按钮A、按钮B、被定位元素C,其中被 元素C 是一个插入到任意地方,且为绝对定位的元素,核心 CSS 代码如下:

<body>
  <div class="btn-a">按钮A</div>
  <div class="btn-b">按钮B</div>
  <div class="anchor">被定位元素C</div>
</div>
.btn-a, .btn-b {
    // 高宽各类样式
}
.anchor {
    position: absolute;
}

现在,三个元素属于同级关系,且 C 元素是绝对定位,当前基于 body 进行绝对定位。

大致样式如下:

而锚点定位的属性的核心作用就是,能够改变元素定位的基准,增强元素的绝对定位的能力。Anchor Positioning(锚点定位)允许我们基于其它锚点元素的位置和尺寸去定位上下文,而不是传统意义上的基于父元素去进行绝对定位。

下面,我们利用锚点定位去实现,当两个按钮 A、B 被 Hover 的时候,让定位元素 C 基于当前被 Hover 的按钮元素进行绝对定位,核心 CSS 代码如下:

.btn-a {
    // 将元素声明为定位基准点,命名为 --btn-a
    anchor-name: --btn-a;
}

.btn-b {
    // 将元素声明为定位基准点,命名为 --btn-b
    anchor-name: --btn-b;
}

// 当元素被 hover 的时候,改变 C 元素的样式
.btn-a:hover ~ .anchor {
    // 锚点绑定,建立元素与锚点的定位关系 
    position-anchor: --btn-a;
    // 基于新的锚点元素,设置元素的 left\top 属性
    left: anchor(--btn-a center);
    top: anchor(--btn-a bottom);
    // 利用 transform 轻微调整定位,非核心代码
    transform: translate(-50%, 5px);
}

// 同理,与上面做的事情一致,知识在 hover 按钮 B 时,重新设定锚点元素
.btn-b:hover ~ .anchor {
    position-anchor: --btn-b;
    left: anchor(--btn-b center);
    top: anchor(--btn-b bottom);
    transform: translate(-50%, 5px);
}

.anchor {
    // 方便动图演示,增加元素过渡动画,非核心代码
    transition: all .2s;
}

核心关注上面的 anchor-nameposition-anchoranchor 几个属性,其作用和含义在注释中有说明,在下文还会再描述一次。

如此一来,我们就实现了动态改变 C 元素定位基准的能力,我们看看效果:

简单而言,我们利用锚点定位的能力,在 hover 按钮 A\B 的时候,把 C 元素定位在它们正下方。这个就是锚点定位的能力!

Anchor Positioning 锚点定位核心 API

(1)anchor-name:锚点定义

功能:将元素声明为定位基准点
语法anchor-name: <dashed-ident>;
应用场景:触发元素、参考元素、动态定位源

/* 定义触发元素为锚点 */
.trigger-btn {
  anchor-name: --menu-anchor;
}

(2)position-anchor:锚点绑定

功能:建立元素与锚点的定位关系
语法position-anchor: <dashed-ident>;
注意:需配合定位属性(position: fixed/absolute)使用

.tooltip {
  position: fixed;
  position-anchor: --menu-anchor; /* 绑定到指定锚点 */
}

(3)anchor():动态定位

功能:根据锚点位置计算坐标
语法anchor(<anchor-name>? <anchor-side>)
方位参数

  • 垂直:top/center/bottom
  • 水平:left/center/right
  • 组合:top-left/bottom-right 等
.context-menu {
  /* 锚点右下角对齐 */
  top: anchor(--ctx-anchor bottom);
  left: anchor(--ctx-anchor right);
}

.tooltip {
  /* 水平居中于锚点 */
  left: anchor(center);
  right: anchor(center);
}

(4)anchor-size():尺寸继承

功能:获取锚点元素的尺寸值
语法anchor-size(<anchor-name>? <dimension>)
维度参数:width/height/block/inline

.popover {
  /* 继承锚点宽度 */
  width: anchor-size(width);
  
  /* 最小高度为锚点高度的1.5倍 */
  min-height: calc(anchor-size(height) * 1.5);
}

这里介绍了锚点定位中最为核心的几个属性,掌握了这几个属性,就可以应付大部分场景了。在我之前的一篇入门文章中,对它们也有一些更为详细的描述,感兴趣的同学,可以翻看:抢先体验!超强大的 Anchor Positioning 锚点定位

锚点定位的候补位置

还有一个非常重要的点,传统的 Popover 组件,一般都会有这么个功能 -- 智能边界处理

我们以一个功能比较强大的 Popover 库 floating-ui 举例,其官网展示了如下的一个功能,当元素在滚动过程中,如果原本 Popover 弹窗被遮挡,会自动进行位置移动,将弹窗重新调整到可视区域,效果如下:

令人振奋的是,现在,CSS 的 Anchor Positioning 锚点定位同样支持这种 智能边界处理,在锚点定位中,我们称之为候补位置。

这里,我们主要借助两个锚点定位相关的属性完成锚点定位的候补位置 position-try-fallbacks@position-try 规则。

@position-try 规则

@position-try 用于定义一个备选定位策略(一组定位规则),可以在多个元素中复用。它的语法类似于定义一个命名规则集合。

/* 语法示例:*/
@position-try --strategy-name {
  /* 具体的定位规则 */
  top: anchor(bottom);
  left: anchor(left);
}

关键点:

  • 命名策略:每个 @position-try 规则需要唯一名称(如 --tooltip-below)。
  • 独立作用域:策略内部的定位规则独立于元素自身的样式,仅在被调用时生效。

position-try-fallbacks 属性

position-try-fallbacks 用于在元素上指定备选定位策略的优先级顺序。浏览器会按顺序尝试这些策略,直到找到第一个可用的位置。

/* 语法示例:*/
.element {
  position-try-options: --strategy1, --strategy2, --strategy3;
}

关键点:

  • 顺序敏感:浏览器按列表顺序尝试策略,第一个可行的策略会被应用。
  • 动态回退:如果所有策略均不可行,元素会回退到默认定位(或父容器约束)。

使用锚点定位实现候补位置

基于上述介绍,我们来实现一个基于锚点定位的候补位置。

假设我们如下结构,当前已经使用了锚点定位:

<body>
  <div class="btn">Reference</div>
  <div class="anchor">Popover弹窗元素</div>
</div>

核心 CSS 如下:

.btn {
    anchor-name: --btn;
    border: 1px dashed #000;
    background: #ddd;
}

.anchor {
    position: absolute;
    position-anchor: --btn;
    left: anchor(--btn-a center);
    top: anchor(--btn-a bottom);
    transform: translate(-50%, 5px);
}

此时,两个元素都插入在 <body> 下面,但是 Popover 弹窗元素使用了 position-anchor: --btn 锚点定位,使其绝对定位的父元素是 .btn,并且,定位在按钮的下方,效果如下:

此时,我们只需要再借助 position-try-fallbacks@position-try,实现候补位置:

  1. @position-try 定义一个候补规则
  2. position-try-fallbacks 引入候补规则

核心 CSS 代码如下:

.btn {
    anchor-name: --btn;
    border: 1px dashed #000;
    background: #ddd;
}

.anchor {
    position: absolute;
    position-anchor: --btn;
    left: anchor(--btn-a center);
    top: anchor(--btn-a bottom);
    transform: translate(-50%, 5px);
    position-try-fallbacks: --position-bottom;
}

@position-try --position-bottom {
    left: anchor(--btn center);
    bottom: anchor(--btn top);
    top: unset;
    margin-bottom: 10px;
}

这样,我们在滚动页面的过程中,如果弹窗 popover 有超出视窗,候补规则会自动生效,看看效果:

仔细观察,和上面利用 Javascript 库实现的智能定位,效果一致,只是此时,我们仅仅使用了寥寥几行 CSS 代码!Amazing!

综上所述,到今天,我们已经可以利用 CSS 锚点定位大致实现一个极简版的 Popover 弹窗,并且可以满足大部分场景。不得不感叹 CSS 确实愈发的强力,

当然,本文介绍的关于锚点定位的功能都是基于实现一个最小版本的 Popover 展开的,基于锚点定位的 API 和回退候补,还有更多有趣的内容,感兴趣的可以猛戳 MDN 进行了解:MDN - Anchor PositioningMDN-Anchor Positioning @position try

最后

好了,本文到此结束,一个非常有意思的 CSS 技巧,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions