使用 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 Anchor Positioning 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 来保持位置正确。这意味着,您和您的团队需要熟悉该软件包及其配置选项。因此,您的问题和决定可能不会减少,而是会改变。这是“为什么”。这样,您在计算排名时便无需考虑效果问题。

使用“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 轴)视口做出反应。仔细阅读文档,您可能会找到适合您的解决方案。

但是,找到适合您项目的软件包可能需要大量时间。这需要额外的决定,如果不能完全达到您想要的效果,可能会令人沮丧。

使用锚点定位

输入 CSS Anchor Positioning 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-name 值的 anchor-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 值,您可以免费获得该翻转。例如,如果您对 bottom 使用 anchor,请将 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 可简化操作。但是,作为起点的 CSS 可能如下所示:

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

结合使用 Popover API 的功能与 CSS Anchor 定位,您就很接近了。

当你开始引入 :has() 之类的元素时,这件事是件很棒的事。您可以在打开时旋转标记:

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

接下来您能做些什么?我们还需要什么才能使其成为正常运行的 select?保存下来,在下一篇文章中使用。不过别担心,我们即将推出可设置样式的 select 元素。敬请期待!


大功告成!

Web 平台正在不断发展。CSS 锚点定位对于改进界面控件开发方式来说是至关重要的一环。这样你就能避免一些棘手的决定。但同时还能让您实现一些以前无法做到的事情。例如,设置 <select> 元素的样式!请与我们分享您的想法。

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