使用 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 锚点定位 API 旨在通过为锚定元素提供 CSS API 来解决此问题。它提供了一种根据其他元素的位置和大小来设置某个元素的位置和大小的方法。

图片显示了一个模拟的浏览器窗口,详细说明了工具提示的结构。

浏览器支持

您可以在 Chrome Canary 中通过“Experimental Web Platform Features”(实验性 Web 平台功能)标志试用 CSS 锚点定位 API。如需启用该标志,请打开 Chrome Canary 并访问 chrome://flags。然后,启用“实验性网站平台功能”标志。

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 轴视口范围的操作不会做出响应。深入了解文档,您可能会找到适合您的解决方案。

不过,找到适合您项目的软件包可能需要很长时间。这需要您做出额外的决策,如果效果不尽如人意,可能会令人沮丧。

使用锚点定位

输入 CSS 锚点定位 API。其基本思想是将样式保留在 CSS 中,并减少您需要做出的决策数量。您希望实现相同的结果,但目标是改善开发者体验。

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

我们来重现并解决上面尝试解决的问题。但请改用船与锚的类比。这些表示锚定元素和锚点。水代表包含的块。

首先,您需要选择定义锚点的方式。您可以在 CSS 中执行此操作,只需在锚点元素上设置 anchor-name 属性即可。它接受带短划线的标识符值。

.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-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 inset 属性,因此如果您知道锚定元素的大小,可以使用 calc。为什么不使用 translate?您可以使用以下方法:

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

但是,浏览器不会考虑锚定元素的转换位置。在考虑位置回退和自动定位时,您会明白这一点的重要性。

您可能已经注意到,上面使用了自定义属性 --boat-size。不过,如果您想根据锚点的大小设置锚定元素的大小,也可以访问该大小。您可以使用 anchor-size 函数,而无需自行计算。例如,若要将船的宽度设为锚的宽度的四倍,请执行以下操作:

.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 创建上下文菜单开始。其基本思路是,点击带有箭头的按钮会显示上下文菜单。该菜单还会包含自己的菜单,可供展开。

标记代码不是重点。但是,您有三个按钮,每个按钮都使用 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 锚点定位相结合,您就离成功不远了。

当您开始介绍 :has() 等内容时,这会很有用。您可以在打开标记后旋转标记:

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

您接下来可以将其带到哪里?为了使其成为一个正常运行的 select,我们还需要做些什么?我们将在下一篇文章中介绍该方法。不过别担心,可设置样式的 select 元素即将推出。敬请期待!


大功告成!

Web 平台在不断发展。CSS 锚点定位对于改进界面控件开发方式至关重要。它可以帮助您避免一些棘手的决策。但它还可以让您执行以前无法执行的操作。例如设置 <select> 元素的样式!请与我们分享您的想法。

照片由 CHUTTERSNAP 拍摄,选自 Unsplash