使用 CSS 锚点定位功能将元素相互共享

您目前如何将一个元素绑定到另一个元素?您可以尝试跟踪其位置,或使用某种形式的封装容器元素。

<!-- index.html -->
<div class="container">
  <a href="/link" class="anchor">I’m the anchor</a>
  <div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
  position: relative;
}
.anchored {
  position: absolute;
}

这些解决方案通常都不理想。它们需要使用 JavaScript 或引入额外的标记。CSS Anchoring API 旨在通过为网络共享元素提供 CSS API 来解决此问题。它提供了一种根据其他元素的位置和大小来设定元素的位置和大小的方法。

图片显示一个模拟浏览器窗口,详细说明了提示的剖析。

浏览器支持

您可以在 Chrome Canary 版的“实验性网络平台功能”标记后面试用 CSS Anchor Positioning API。如需启用该标志,请打开 Chrome Canary 并访问 chrome://flags。然后启用“实验性 Web 平台功能”标志。

Oddbird 的团队也正在开发 polyfill。请务必在 github.com/oddbird/css-anchor-positioning 上查看代码库。

您可以通过以下方式检查锚定支持情况:

@supports(anchor-name: --foo) {
  /* Styles... */
}

请注意,此 API 仍处于实验阶段,可能会发生变化。本文将概括介绍这些重要部分。当前的实现也与 CSS 工作组规范并不完全相符。

问题

您为什么需要这样做?一个突出的应用场景是创建提示或类似于提示的体验。在这种情况下,您通常需要将提示与其引用的内容进行网络共享。通常需要通过某种方式将一个元素绑定至另一个元素。此外,您也希望与网页互动不会中断该绑定,例如,当用户滚动屏幕或调整界面大小时。

另一个问题是,您是否想要确保网络共享元素保持在视图中,例如,在您打开提示后,它被视口边界裁剪。因此,这对用户来说可能不是很棒的体验。您希望提示进行调整。

当前解决方案

目前,您可以通过一些不同的方法解决此问题。

首先是基本的“封装锚点”方法。您可以获取两个元素,然后将它们封装在容器中。然后,您可以使用 position 相对于锚点来定位提示。

<div class="containing-block">
  <div class="tooltip">Anchor me!</div>
  <a class="anchor">The anchor</a>
</div>
.containing-block {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
}

您可以移动容器,这样所有元素在大多数情况下都会保留在所需的位置。

另一种方法是,如果您知道锚点的位置,或者能够以某种方式对其进行跟踪。您可以使用自定义属性将其传递给提示。

<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
  --anchor-width: 120px;
  --anchor-top: 40vh;
  --anchor-left: 20vmin;
}

.anchor {
  position: absolute;
  top: var(--anchor-top);
  left: var(--anchor-left);
  width: var(--anchor-width);
}

.tooltip {
  position: absolute;
  top: calc(var(--anchor-top));
  left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
  transform: translate(-50%, calc(-100% - 10px));
}

但是,如果您不知道锚点的位置,该怎么办?您可能需要干预 JavaScript。您可以执行类似于以下代码的操作,但现在这意味着您的样式开始从 CSS 泄露到 JavaScript 中。

const setAnchorPosition = (anchored, anchor) => {
  const bounds = anchor.getBoundingClientRect().toJSON();
  for (const [key, value] of Object.entries(bounds)) {
    anchored.style.setProperty(`--${key}`, value);
  }
};

const update = () => {
  setAnchorPosition(
    document.querySelector('.tooltip'),
    document.querySelector('.anchor')
  );
};

window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);

这就开始产生一些问题:

  • 何时计算样式?
  • 如何计算样式?
  • 我多久计算一次样式?

问题解决了吗?它可能适用于您的用例,但有一个问题:我们的解决方案无法进行调整。设备没有响应。如果锚定元素被视口截断,该怎么办?

现在,您需要决定是否对此以及应如何回应。你需要做的问题和决策的数量开始增加。您只需将一个元素锚定到另一个元素即可。在理想世界中,你的解决方案会进行调整并对周围环境做出反应。

为了减轻一些痛苦,你可以寻求 JavaScript 解决方案来帮忙。这会产生向项目添加依赖项的成本,并且可能会因您使用依赖项的方式引入性能问题。例如,某些软件包使用 requestAnimationFrame 来保持正确的位置。也就是说,您和您的团队需要熟悉该软件包及其配置选项。因此,您的问题和决定可能不会减少,反而会发生变化。这是 CSS 锚点定位“为什么”的一部分。这样,在计算排名时,您就不必考虑效果问题了。

使用“floating-ui”(一种用于解决此问题的热门软件包)时,代码可能如下所示:

import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';

const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')

const updatePosition = () => {  
  computePosition(anchor, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip()]
  })
    .then(({x, y}) => {
      Object.assign(tooltip.style, {
        left: `${x}px`,
        top: `${y}px`
      })
  })
};

const clean = autoUpdate(anchor, tooltip, updatePosition);

在此使用该代码的演示中,尝试重新定位锚点。

“提示”可能会有不符合您的预期的行为。当 y 轴离开视口时,x 轴会做出响应,而 x 轴则不会做出响应。请仔细阅读相关文档,您很可能会找到适合您的解决方案。

不过,找到适合您项目的软件包可能需要很长时间。这种决定是额外的决定,如果事与你的愿望不符,你可能会感到沮丧。

使用锚点定位

不妨输入 CSS Anchoring API。其理念是将您的样式保留在 CSS 中,并减少需要做出的决策数量。您希望达到相同的结果,但目标是改善开发者体验。

  • 无需 JavaScript。
  • 让浏览器根据您的指导确定最佳位置。
  • 没有更多第三方依赖项
  • 无封装容器元素。
  • 处理位于顶层的元素。

我们来重新创建并解决我们在上面尝试解决的问题。但是,我们只使用带有锚的船来比喻。它们表示锚定元素和锚点。水表示容器块。

首先,您需要选择如何定义锚点。您可以在 CSS 中执行此操作,只需对锚点元素设置 anchor-name 属性即可。它接受 dashed-ident 值。

.anchor {
  anchor-name: --my-anchor;
}

或者,您也可以使用 anchor 属性定义 HTML 中的锚点。属性值是锚点元素的 ID。这将创建一个隐式锚点。

<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>

定义锚点后,您可以使用 anchor 函数。anchor 函数采用 3 个参数:

  • Anchor 元素:要使用的锚点的 anchor-name;或者,您可以省略值以使用 implicit 锚点。可以通过 HTML 关系来定义它,也可以使用值为 anchor-nameanchor-default 属性来定义。
  • 锚点端:您想要使用的位置的关键字。可以是 toprightbottomleftcenter 等。或者,您也可以传递百分比。例如,50% 等于 center
  • 后备值:这是一个可选后备值,接受长度或百分比。

您可以使用 anchor 函数作为锚定元素的边衬区属性toprightbottomleft 或其逻辑等效项)的值。您还可以在 calc 中使用 anchor 函数:

.boat {
  bottom: anchor(--my-anchor top);
  left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}

 /* alternative with anchor-default */
.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: calc(anchor(center) - (var(--boat-size) * 0.5));
}

没有 center 边衬区属性,因此如果您知道锚定元素的尺寸,可以选择使用 calc。为什么不使用 translate?您可以使用以下代码:

.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
}

但是,浏览器不会考虑锚定元素的转换位置。将清楚为什么在考虑使用排名后备和自动定位时这很重要。

您可能已经注意到上述使用自定义属性 --boat-size。但是,如果您希望锚定元素的尺寸基于锚点的大小,也可以使用该尺寸。您可以使用 anchor-size 函数,而不是自行计算。例如,要将船粗细为锚点的 4 倍:

.boat {
  width: calc(4 * anchor-size(--my-anchor width));
}

您还可以使用 anchor-size(--my-anchor height) 获取高度。您可以使用它来设置任一轴的大小,或同时设置两者。

如果您想使用 absolute 定位功能锚定到某个元素,该怎么办?根据规则,元素不能是同级元素。在这种情况下,您可以使用具有 relative 定位的容器来封装锚点。然后,您就可以锚定到该素材资源了。

<div class="anchor-wrapper">
  <a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>

查看此演示,您可以在其中拖动锚点,船将跟随。

跟踪滚动位置

在某些情况下,锚点元素可能位于滚动容器中。同时,锚定元素可能位于该容器之外。由于滚动发生在与布局不同的线程上,因此您需要提供一种方法来跟踪它。anchor-scroll 属性可以执行此操作。您需要在锚定元素上设置该元素,并为其提供要跟踪的锚点值。

.boat { anchor-scroll: --my-anchor; }

尝试此演示,您可以使用角落中的复选框来开启和关闭 anchor-scroll

这个比喻在这有点平坦,但在理想世界中,您的船和锚均在水中。此外,Popover API 等功能还有助于使相关元素保持邻近。不过,锚点定位也适用于顶层中的元素。这是此 API 的主要优势之一:能够将不同流中的元素绑定在一起。

请看下面的演示,它有一个滚动容器,其中包含带有提示的锚点。弹出式窗口形式的提示元素可能与锚点不位于同一位置:

不过,您会注意到弹出式窗口如何跟踪其各自的锚链接。您可以调整滚动容器的大小,系统会自动更新其位置。

位置后备和自动定位

这时锚定定位功能会进一步提升。position-fallback 可以根据您提供的一组回退来放置锚定元素。您可根据自己的样式来引导浏览器,让它为您确定合适的位置。

此处的常见用例是一条提示,它应在锚点上方和下方显示。此行为基于提示是否会被其容器裁剪。该容器通常是视口。

如果你深入研究上一个演示的代码,你会看到正在使用 position-fallback 属性。如果您滚动了容器,可能注意到这些锚定弹出式窗口发生了跳跃。当其各自的锚点靠近视口边界时,就会发生这种情况。此时,弹出式窗口会尝试进行调整,以留在视口内。

在创建显式 position-fallback 之前,锚点定位还将提供自动定位。您可以通过在锚点函数和相反的边衬区属性中使用值 auto 免费获得该翻转效果。例如,如果您使用 anchor 作为 bottom,则将 top 设置为 auto

.tooltip {
  position: absolute;
  bottom: anchor(--my-anchor auto);
  top: auto;
}

自动定位的替代方案是使用显式 position-fallback。为此,您需要定义一个排名后备集合。浏览器将逐一查找这些位置,直到找到可以使用的位置,然后再应用该位置。如果找不到有效的函数,则默认为定义的第一个函数。

如果 position-fallback 尝试在上方和下方显示提示,可能如下所示:

@position-fallback --top-to-bottom {
  @try {
    bottom: anchor(top);
    left: anchor(center);
  }

  @try {
    top: anchor(bottom);
    left: anchor(center);
  }
}

将其应用于提示如下所示:

.tooltip {
  anchor-default: --my-anchor;
  position-fallback: --top-to-bottom;
}

使用 anchor-default 意味着您可以为其他元素重复使用 position-fallback。您还可以使用限定了作用域的自定义属性来设置 anchor-default

再次考虑这个使用船的演示。有一个 position-fallback 集。随着您改变锚的位置,船会调整以留在容器中。尝试也更改内边距值,以便调整正文的内边距。请注意浏览器是如何更正定位的。通过更改容器的网格对齐方式,可以更改位置。

这次 position-fallback 尝试按顺时针方向确定位置更为冗长。

.boat {
  anchor-default: --my-anchor;
  position-fallback: --compass;
}

@position-fallback --compass {
  @try {
    bottom: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    right: anchor(left);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }
}


示例

现在,您已经了解了锚定定位的主要功能,接下来我们看看提示以外的一些有趣的示例。这些示例旨在让您尽情思考锚定定位的使用方式。完善规范的最佳方法是参考像您这样的真实用户的意见。

上下文菜单

我们先从使用 Popover API 的上下文菜单开始。具体而言,点击 V 形按钮会显示上下文菜单。该菜单将有自己的菜单供您展开。

标记在这里并不是重要部分,但是,您有三个使用 popovertarget 的按钮。然后,您有三个使用 popover 属性的元素。这样,您无需使用任何 JavaScript 即可打开上下文菜单。您可以如下所示:

<button popovertarget="context">
  Toggle Menu
</button>        
<div popover="auto" id="context">
  <ul>
    <li><button>Save to your Liked Songs</button></li>
    <li>
      <button popovertarget="playlist">
        Add to Playlist
      </button>
    </li>
    <li>
      <button popovertarget="share">
        Share
      </button>
    </li>
  </ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>

现在,您可以定义 position-fallback 并在上下文菜单之间共享它。确保也取消为弹出式窗口设置任何 inset 样式。

[popovertarget="share"] {
  anchor-name: --share;
}

[popovertarget="playlist"] {
  anchor-name: --playlist;
}

[popovertarget="context"] {
  anchor-name: --context;
}

#share {
  anchor-default: --share;
  position-fallback: --aligned;
}

#playlist {
  anchor-default: --playlist;
  position-fallback: --aligned;
}

#context {
  anchor-default: --context;
  position-fallback: --flip;
}

@position-fallback --aligned {
  @try {
    top: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }

  @try {
    top: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(bottom);
    left: anchor(right);
  }

  @try {
    right: anchor(left);
    bottom: anchor(bottom);
  }
}

@position-fallback --flip {
  @try {
    bottom: anchor(top);
    left: anchor(left);
  }

  @try {
    right: anchor(right);
    bottom: anchor(top);
  }

  @try {
    top: anchor(bottom);
    left: anchor(left);
  }

  @try {
    top: anchor(bottom);
    right: anchor(right);
  }
}

这将为您提供自适应嵌套上下文菜单界面。尝试使用选择来更改内容位置。您选择的选项会更新网格对齐方式。这会影响锚点定位如何定位弹出式窗口。

专注并遵循

此演示通过引入 :has() 来组合 CSS 基元。具体思路是为获得焦点的 input 转换视觉指示器

为此,可以在运行时设置新锚点。在本演示中,限定了范围的自定义属性会在输入焦点上更新。

#email {
    anchor-name: --email;
  }
  #name {
    anchor-name: --name;
  }
  #password {
    anchor-name: --password;
  }
:root:has(#email:focus) {
    --active-anchor: --email;
  }
  :root:has(#name:focus) {
    --active-anchor: --name;
  }
  :root:has(#password:focus) {
    --active-anchor: --password;
  }

:root {
    --active-anchor: --name;
    --active-left: anchor(var(--active-anchor) right);
    --active-top: calc(
      anchor(var(--active-anchor) top) +
        (
          (
              anchor(var(--active-anchor) bottom) -
                anchor(var(--active-anchor) top)
            ) * 0.5
        )
    );
  }
.form-indicator {
    left: var(--active-left);
    top: var(--active-top);
    transition: all 0.2s;
}

但是,您如何才能更进一步呢?您可以将其用于某种形式的指导性叠加层。提示可以在地图注点之间移动,并更新其内容。您可以淡入淡出内容。您可以使用离散动画来display 添加动画效果视图过渡

条形图计算

使用锚点定位可以实现的另一个有趣操作是将其与 calc 结合使用。假设在某个图表中,有一些用于为图表添加注释的弹出式窗口。

您可以使用 CSS minmax 跟踪最高值和最低值。其 CSS 可能会如下所示:

.chart__tooltip--max {
    left: anchor(--chart right);
    bottom: max(
      anchor(--anchor-1 top),
      anchor(--anchor-2 top),
      anchor(--anchor-3 top)
    );
    translate: 0 50%;
  }

需要使用一些 JavaScript 来更新图表值,并使用一些 CSS 来设置图表样式。但锚点定位会为我们处理布局更新。

调整手柄大小

您不必只锚定到一个元素。您可以为一个元素使用多个锚点。您可能已经注意到,在条形图示例中。提示已经固定在图表和相应的条形上。如果您对该概念进行更深入的研究,则可以使用它来调整元素的大小。

您可以将锚点视为自定义的大小调整手柄,并采用 inset 值。

.container {
   position: absolute;
   inset:
     anchor(--handle-1 top)
     anchor(--handle-2 right)
     anchor(--handle-2 bottom)
     anchor(--handle-1 left);
 }

在此演示中,GreenSock Draggable 使句柄可拖动。不过,<img> 元素会调整大小,以填充容器,而容器也会调整以填满手柄之间的间隙。

一个 SelectMenu?

最后一张只是提醒接下来会发生什么。但是,您可以创建一个可聚焦的弹出式窗口,现在您便拥有了锚点定位功能。您可以为可设置样式的 <select> 元素创建基础。

<div class="select-menu">
<button popovertarget="listbox">
 Select option
 <svg>...</svg>
</button>
<div popover="auto" id="listbox">
   <option>A</option>
   <option>Styled</option>
   <option>Select</option>
</div>
</div>

隐式 anchor 会让这变得更简单。但最基本的起点可能如下所示:

[popovertarget] {
 anchor-name: --select-button;
}
[popover] {
  anchor-default: --select-button;
  top: anchor(bottom);
  width: anchor-size(width);
  left: anchor(left);
}

将 Popover API 的功能与 CSS 锚点定位相结合,就大功告成了。

建议你先介绍一下“:has()”之类的内容。您可以在打开时旋转标记:

.select-menu:has(:open) svg {
  rotate: 180deg;
}

您该如何继续开展这项工作?我们还需要做些什么才能使其成为一个正常运行的 select?我们会保存这项信息,留到下一篇文章中。不过别担心,我们即将推出可设置样式的精选元素。敬请期待!


大功告成!

Web 平台在不断发展。CSS 锚点定位对于改进界面控件的开发方式至关重要。这会使您摆脱一些棘手的决定。但同时也能让您做一些之前从未做过的事情。例如,设置 <select> 元素的样式!请与我们分享你的想法。

照片提供者:CHUTTERSNAP,来源:Unsplash