使用 CSS 锚点定位

CSS 锚点定位模块定义了一些功能,允许你将元素相互绑定。元素可以被定义为锚点元素锚点定位元素。锚点定位元素可以绑定到锚点元素上。然后,锚点定位元素的大小和位置可以相对于其绑定的锚点元素的大小和位置进行设置。

CSS 锚点定位还提供了纯 CSS 机制来为一个锚点定位元素指定多个备选位置。例如,如果一个工具提示框锚定到一个表单字段,但在其默认位置设置下会渲染到屏幕外,浏览器可以尝试在另一个建议的位置渲染它,以便它能显示在屏幕上,或者,如果需要,也可以完全隐藏它。

本文解释了锚点定位的基本概念,以及如何基础地使用该模块的关联、定位和尺寸调整功能。对于下面讨论的每个概念,我们都包含了指向参考页面的链接,其中包含额外的示例和语法细节。有关指定备选位置和隐藏锚点定位元素的信息,请参阅溢出时的回退选项和条件隐藏指南。

基本概念

将一个元素绑定或关联到另一个元素的需求非常普遍。例如:

  • 显示在表单控件旁边的错误信息。
  • 出现在 UI 元素旁边的工具提示或信息框,以提供更多相关信息。
  • 可以访问的设置或选项对话框,用于快速配置 UI 元素。
  • 出现在关联的导航栏或按钮旁边的下拉菜单或弹出菜单。

现代界面经常需要将某些内容(通常是可重用且动态生成的)相对于一个锚点元素进行定位。如果被绑定的元素(即锚点元素)总是在 UI 中的相同位置,并且被绑定的元素(即锚点定位元素,或简称为定位元素)总能在源顺序中紧随其前或其后,那么创建这样的用例将相当简单。然而,事情很少这么简单。

定位元素相对于其锚点元素的位置需要随着锚点元素的移动或配置变化(例如,通过滚动、改变视口大小、拖放等)而保持和调整。例如,如果一个元素(如表单字段)靠近视口边缘,其工具提示可能会出现在屏幕外。通常,你希望将工具提示绑定到其表单控件,并确保只要表单字段可见,工具提示就完全保持在屏幕上,必要时自动移动工具提示。你可能已经注意到,这在你桌面或笔记本电脑上右键单击(Ctrl + 单击)上下文菜单时是操作系统的默认行为。

历史上,将一个元素与另一个元素关联,并根据锚点的位置动态改变定位元素的位置和大小需要 JavaScript,这增加了复杂性和性能问题。而且,也无法保证在所有情况下都能正常工作。CSS 锚点定位模块中定义的功能使得能够用 CSS(和 HTML)以高性能和声明式的方式实现这些用例,而无需使用 JavaScript。

关联锚点和定位元素

要将一个元素与一个锚点关联起来,你需要首先声明哪个元素是锚点,然后指定哪个定位元素要与该锚点关联。这就在两者之间创建了一个锚点引用。这种关联可以通过 CSS 显式创建,也可以隐式创建。

显式 CSS 锚点关联

要用 CSS 将一个元素声明为锚点,你需要通过 anchor-name 属性为其设置一个锚点名称。锚点名称必须是一个 <dashed-ident>。在这个例子中,我们还将锚点的 width 设置为 fit-content 以获得一个小方块锚点,这样能更好地展示锚定效果。

css
.anchor {
  anchor-name: --my-anchor;
  width: fit-content;
}

将一个元素转换为锚点定位元素需要两步:它需要使用 position 属性进行绝对或固定定位。然后,将定位元素的 position-anchor 属性设置为锚点元素的 anchor-name 属性的值,以将两者关联起来。

css
.infobox {
  position: fixed;
  position-anchor: --my-anchor;
}

我们将把上面的 CSS 应用到下面的 HTML 中:

html
<div class="anchor">⚓︎</div>

<div class="infobox">
  <p>This is an information box.</p>
</div>

这将渲染如下:

锚点和信息框现在已经关联起来了,但目前你只能相信我们的话。它们还没有被绑定在一起——如果你定位锚点并将其移动到页面的其他地方,它会自己移动,而信息框会留在原地。当我们在基于锚点位置定位元素中看到实际的绑定效果时,你就会明白了。

隐式锚点关联

在某些情况下,由于两个元素之间语义上的关系,它们之间会建立一个隐式的锚点引用:

  • 当使用 Popover API 将一个弹出框与一个控件关联时,两者之间会建立一个隐式的锚点引用。这可能发生在:
    • 使用 popovertargetid 属性,或 commandforid 属性,以声明方式将弹出框与控件关联。
    • 使用 source 选项,以编程方式将一个弹出框操作(如 showPopover())与一个控件关联。
  • 一个 <select> 元素及其下拉选择器通过 appearance 属性的 base-select 值启用了可自定义 select 元素功能。在这种情况下,两者之间会创建一个隐式的弹出框-调用者关系,这也意味着它们将有一个隐式的锚点引用。

注意: 上述方法将锚点与一个元素关联起来,但它们尚未被绑定。要将它们绑定在一起,定位元素需要相对于其锚点进行定位,这是用 CSS 完成的。

移除锚点关联

如果你希望移除先前在锚点元素和定位元素之间建立的显式锚点关联,你可以执行以下操作之一:

  1. 将锚点的 anchor-name 属性值设置为 none,或者设置为一个不同的 <dashed-ident>(如果你想让另一个元素锚定到它)。
  2. 将定位元素的 position-anchor 属性设置为当前文档中不存在的锚点名称,例如 --not-an-anchor-name

然而,在隐式锚点关联的情况下,你需要使用第二种方法——第一种方法不起作用。这是因为关联是内部控制的,你无法通过 CSS 移除 anchor-name

例如,要阻止一个可自定义 <select> 元素的选择器锚定到 <select> 元素本身,你可以使用以下规则:

css
::picker(select) {
  position-anchor: --not-an-anchor-name;
}

相对于锚点定位元素

正如我们上面所见,将一个定位元素与一个锚点关联起来本身并没有太大用处。我们的目标是将定位元素相对于其关联的锚点元素进行放置。这可以通过在内边距属性上设置一个 CSS anchor() 函数值、指定一个 position-area,或者使用 anchor-center 放置值将定位元素居中来实现。

注意: CSS 锚点定位还提供了指定回退位置的机制,以防定位元素的默认位置导致其溢出视口。详情请参阅回退选项和条件隐藏指南。

注意: 锚点元素必须是一个可见的 DOM 节点,关联和定位才能起作用。如果它被隐藏(例如通过 display: none),定位元素将相对于其最近的已定位祖先进行定位。我们在使用 position-visibility 进行条件隐藏中讨论了当锚点消失时如何隐藏锚点定位元素。

使用带有 anchor() 函数值的内边距属性

传统的绝对定位和固定定位元素是通过在内边距属性上设置 <length><percentage> 值来明确地定位的。对于 position: absolute,这个内边距位置值是相对于最近的已定位祖先的边缘的绝对距离。对于 position: fixed,内边距位置值是相对于视口的绝对距离。

CSS 锚点定位改变了这种模式,使得锚点定位元素可以相对于其关联锚点的边缘进行放置。该模块定义了 anchor() 函数,它是每个内边距属性的有效值。使用时,该函数通过定义锚点元素、定位元素相对于锚点元素的哪一侧以及与该侧的距离,将内边距位置值设置为相对于锚点元素的绝对距离。

函数组件如下所示:

anchor(<anchor-name> <anchor-side>, <fallback>)
<anchor-name>

你希望将元素的一侧定位到其相对的锚点元素的 anchor-name 属性值。这是一个 <dashed-ident> 值。如果省略,将使用元素的默认锚点。这是在其 position-anchor 属性中引用的锚点,或通过非标准的 anchor HTML 属性与元素关联的锚点。

注意: 指定一个 <anchor-name> 会将元素相对于该锚点进行定位,但不会提供元素关联。虽然你可以通过在同一元素上的不同 anchor() 函数中指定不同的 <anchor-name>,将元素的两侧相对于多个锚点进行定位,但定位元素只与单个锚点关联。

<anchor-side>

指定相对于锚点的一侧或多侧的位置。有效值包括锚点的 center、物理侧(topleft 等)或逻辑侧(startself-end 等),或在 anchor() 设置的内边距属性轴的起始(0%)和结束(100%)之间的 <percentage>。如果使用的值与设置 anchor() 函数的内边距属性不兼容,则使用回退值。

<fallback>

一个 <length-percentage>,定义了在元素未进行绝对或固定定位、使用的 <anchor-side> 值与设置 anchor() 函数的内边距属性不兼容,或锚点元素不存在时用作回退值的距离。

anchor() 函数的返回值是一个根据锚点位置计算出的长度值。如果你直接在锚点定位元素的内边距属性上设置长度或百分比,它的定位方式就好像它没有绑定到锚点元素一样。这与 <anchor-side> 值与设置它的内边距属性不兼容并使用回退值时的行为相同。这两个声明是等效的:

css
bottom: anchor(right, 50px);
bottom: 50px;

两者都会将定位元素放置在元素最近的已定位祖先(如果有)或初始包含块底部的上方 50px 处。

你将使用的最常见的 anchor() 参数会引用默认锚点的一侧。你通常也会添加一个 margin 来在锚点和定位元素的边缘之间创建间距,或者在 calc() 函数中使用 anchor() 来添加该间距。

例如,这条规则将定位元素的右边缘与锚点元素的左边缘对齐,然后添加一些 margin-left 来在边缘之间创建一些空间:

css
.positionedElement {
  right: anchor(left);
  margin-left: 10px;
}

anchor() 函数的返回值是一个长度。这意味着你可以在 calc() 函数中使用它。这条规则将定位元素的逻辑块结束边缘定位在距离锚点元素的逻辑块起始边缘 10px 的位置,使用 calc() 函数添加间距,这样我们就不需要添加外边距了:

css
.positionedElement {
  inset-block-end: calc(anchor(start) + 10px);
}

anchor() 示例

让我们来看一个 anchor() 的实际例子。我们使用了与之前示例相同的 HTML,但在其上方和下方放置了一些填充文本,以使其内容溢出容器并产生滚动。我们还会给锚点元素与之前示例中相同的 anchor-name

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

信息框通过锚点名称与锚点关联,并被赋予固定定位。通过包含 inset-block-startinset-inline-start 属性(在水平从左到右的书写模式中,它们分别等同于 topleft),我们将其绑定到了锚点上。我们给信息框添加了一个 margin,以在定位元素和其锚点之间增加空间:

css
.infobox {
  position-anchor: --my-anchor;
  position: fixed;
  inset-block-start: anchor(end);
  inset-inline-start: anchor(self-end);
  margin: 5px 0 0 5px;
}

让我们更详细地看一下内边距属性定位的声明:

  • inset-block-start: anchor(end):这会将定位元素的块起始边缘设置为锚点的块结束边缘,使用 anchor(end) 函数计算。
  • inset-inline-start: anchor(self-end):这会将定位元素的内联起始边缘设置为锚点的内联结束边缘,使用 anchor(self-end) 函数计算。

这给了我们以下结果:

定位元素位于锚点元素下方 5px 和右侧 5px 的位置。如果你上下滚动文档,定位元素会保持其相对于锚点元素的位置——它被固定到锚点元素,而不是视口。

设置 position-area

position-area 属性提供了一种替代 anchor() 函数来相对于锚点定位元素的方法。position-area 属性基于一个 3x3 的网格概念,其中锚点元素是中心单元格。position-area 属性可用于将锚点定位元素放置在九个单元格中的任何一个,或者使其跨越两个或三个单元格。

The position-area grid, as described below

网格瓷砖被分为行和列。

  • 三行由物理值 topcenterbottom 表示。它们也有逻辑等效值,如 startcenterend,以及坐标等效值,如 y-startcentery-end
  • 三列由物理值 leftcenterright 表示。它们也有逻辑等效值,如 startcenterend,以及坐标等效值,如 x-startcenterx-end

中心单元格的尺寸由锚点元素的包含块定义,而中心单元格与网格外边缘之间的距离由定位元素的包含块定义。

position-area 属性值由一到两个基于上述行和列的值组成,并提供跨越选项来定义元素应定位的网格区域。

例如

你可以指定两个值,将定位元素放置在特定的网格单元中。例如:

  • top left(逻辑等效为 start start)会将定位元素放置在左上角的单元格中。
  • bottom center(逻辑等效为 end center)会将定位元素放置在底部中央的单元格中。

你可以指定一个行或列的值,加上一个 span-* 值。第一个值指定了放置定位元素的行或列,初始时将其放置在中心,另一个值指定了要跨越的列的数量。例如:

  • top span-left 会使定位元素被放置在顶行,并跨越该行的中心和左侧单元格。
  • y-end span-x-end 会使定位元素被放置在 y 轴的末端,并跨越该列的中心和 x 轴末端的单元格。
  • block-end span-all 会使定位元素被放置在块结束行,并跨越该行的内联起始、中心和内联结束单元格。

如果你只指定一个值,效果会因设置的值而异:

  • 物理侧边值(topbottomleftright)或坐标值(y-starty-endx-startx-end)的作用如同另一个值是 span-all。例如,top 的效果与 top span-all 相同。
  • 逻辑侧边值(startend)的作用如同另一个值被设置为相同的值;例如,start 的效果与 start start 相同。
  • center 值的作用如同两个值都设置为 center(即 center center)。

注意: 有关所有可用值的详细描述,请参阅 <position-area> 值参考页面。将逻辑值与物理值混合使用将使声明无效。

让我们来演示一些这样的值;这个例子使用了与前一个例子相同的 HTML 和基础 CSS 样式,只是我们增加了一个 <select> 元素,以便能够更改定位元素的 position-area 值。

信息框被赋予固定定位,并使用 CSS 与锚点关联。加载时,它被设置为通过 position-area: top; 绑定到锚点,这使其位于位置区域网格的顶部。一旦你从 <select> 菜单中选择不同的值,这个设置将被覆盖。

css
.infobox {
  position: fixed;
  position-anchor: --my-anchor;
  position-area: top;
}

我们还包含了一个简短的脚本,用于将从 <select> 菜单中选择的新 position-area 值应用到信息框:

js
const infobox = document.querySelector(".infobox");
const selectElem = document.querySelector("select");

selectElem.addEventListener("change", () => {
  const area = selectElem.value;

  // Set the position-area to the value chosen in the select box
  infobox.style.positionArea = area;
});

尝试从 <select> 菜单中选择新的 position-area 值,看看它们对信息框位置的影响:

定位元素的宽度

在上面的示例中,我们没有在任何维度上显式地设置定位元素的尺寸。我们故意省略了尺寸设置,以便让你观察到由此产生的行为。

当一个定位元素被放置到没有显式尺寸的 position-area 网格单元中时,它会与指定的网格区域对齐,其行为就像 width 被设置为 max-content。它的尺寸是根据其包含块的大小来确定的,即其内容的宽度。这个尺寸是由设置 position: fixed 强加的。自动尺寸的绝对定位和固定定位元素会自动调整大小,伸展到足以容纳文本内容,同时受视口边缘的限制。在这种情况下,当使用任何 leftinline-start 值放置在网格的左侧时,文本会换行。如果锚定元素的 max-content 尺寸比其锚点更窄或更短,它们不会增长以匹配锚点的大小。

如果定位元素是垂直居中的,例如使用 position-area: bottom center,它将与指定的网格单元对齐,并且宽度将与锚点元素相同。在这种情况下,它的最小高度是锚点元素的包含块大小。它不会溢出,因为 min-widthmin-content,这意味着它至少会和它最长的单词一样宽。

使用 anchor-center 在锚点上居中

虽然你可以使用 position-areacenter 值来居中锚点定位元素,但内边距属性结合 anchor() 函数可以更精确地控制位置。CSS 锚点定位提供了一种方法,当使用内边距属性(而不是 position-area)进行绑定时,可以相对于其锚点居中一个锚点定位元素。

justify-selfalign-selfjustify-itemsalign-items 属性(以及它们的简写 place-itemsplace-self)的存在是为了让开发者能够在各种布局系统中轻松地在内联或块方向上对齐元素,例如在 flex 子元素的情况下沿着主轴或交叉轴。CSS 锚点定位为这些属性提供了一个额外的值,anchor-center,它将一个定位元素与其默认锚点的中心对齐。

这个例子使用了和前一个例子相同的 HTML 和基础 CSS。信息框被赋予了固定定位,并绑定到锚点的底部边缘。然后使用 justify-self: anchor-center 来确保它在锚点的中心水平居中。

css
.infobox {
  position: fixed;
  position-anchor: --my-anchor;
  top: calc(anchor(bottom) + 5px);
  justify-self: anchor-center;
}

这将锚点定位元素在其锚点底部居中:

基于锚点大小调整元素尺寸

除了相对于锚点的位置来定位元素之外,你还可以使用 anchor-size() 函数在尺寸属性值中,相对于锚点的大小来调整元素的大小。

可以接受 anchor-size() 值的尺寸属性包括:

anchor-size() 函数解析为 <length> 值。它们的语法如下:

anchor-size(<anchor-name> <anchor-size>, <length-percentage>)
<anchor-name>

设置为你想要调整元素大小所依据的锚点元素的 anchor-name 属性值的 <dashed-ident> 名称。如果省略,将使用元素的默认锚点,即在 position-anchor 属性中引用的锚点。

<anchor-size>

指定定位元素将要参照的锚点元素的尺寸。这可以使用物理值(widthheight)或逻辑值(inlineblockself-inlineself-block)来表示。

<length-percentage>

指定在元素不是绝对定位或固定定位,或者锚点元素不存在时用作回退值的尺寸。

你将使用的最常见的 anchor-size() 函数只会引用默认锚点的某个维度。你也可以在 calc() 函数中使用它们,以修改应用于定位元素的尺寸。

例如,此规则将定位元素的宽度设置为等于默认锚点元素的宽度:

css
.elem {
  width: anchor-size(width);
}

这条规则将定位元素的内联尺寸设置为锚点元素内联尺寸的 4 倍,乘法是在 calc() 函数内部完成的:

css
.elem {
  inline-size: calc(anchor-size(self-inline) * 4);
}

让我们看一个例子。HTML 和基础 CSS 与前面的例子相同,只是锚点元素被赋予了 tabindex="0" 属性以使其可聚焦。信息框被赋予固定定位,并以与之前相同的方式与锚点关联。然而,这一次我们使用 position-area 将其绑定到锚点的右侧,并使其宽度为锚点宽度的五倍:

css
.infobox {
  position: fixed;
  position-anchor: --my-anchor;
  position-area: right;
  margin-left: 5px;
  width: calc(anchor-size(width) * 5);
}

此外,我们在 :hover:focus 状态下增加了锚点元素的 width,并为其添加了 transition,以便在状态变化时有动画效果。

css
.anchor {
  text-align: center;
  width: 30px;
  transition: 1s width;
}

.anchor:hover,
.anchor:focus {
  width: 50px;
}

将鼠标悬停在锚点元素上或用 Tab 键聚焦到它——定位元素会随着锚点的增长而增长,这表明锚点定位元素的尺寸是相对于其锚点的:

anchor-size() 的其他用法

你也可以在物理和逻辑的内边距和外边距属性中使用 anchor-size()。下面的部分将更详细地探讨这些用法,然后提供一个使用示例。

基于锚点尺寸设置元素位置

你可以在内边距属性值中使用 anchor-size() 函数来根据其锚点元素的尺寸定位元素,例如:

css
left: anchor-size(width);
inset-inline-end: anchor-size(--my-anchor height, 100px);

这并不会像 anchor() 函数或 position-area 属性那样,根据锚点的位置来定位一个元素(参见上面的相对于锚点定位元素);当锚点移动时,元素的位置不会改变。相反,元素将根据 absolutefixed 定位的正常规则进行定位。

这在某些情况下可能很有用。例如,如果你的锚点元素只能垂直移动,并且总是水平地保持在其最近的已定位祖先的边缘旁边,你可以使用 left: anchor-size(width) 来使锚点定位元素总是位于其锚点的右侧,即使锚点宽度发生变化。

基于锚点尺寸设置元素外边距

你可以在 margin-* 属性值中使用 anchor-size() 函数,根据其锚点元素的尺寸来设置元素的外边距,例如:

css
margin-left: calc(anchor-size(width) / 4);
margin-block-start: anchor-size(--my-anchor self-block, 20px);

这在某些情况下很有用,例如当你希望锚点定位元素的外边距始终等于锚点元素宽度的相同百分比时,即使宽度发生变化。

anchor-size() 位置和外边距示例

让我们看一个例子,其中我们根据锚点元素的宽度来设置锚点定位元素的外边距和位置。

在 HTML 中,我们指定了两个 <div> 元素,一个 anchor 元素和一个 infobox 元素,我们将相对于锚点来定位它。我们给锚点元素一个 tabindex 属性,以便可以通过键盘聚焦。我们还包含了一些填充文本,使 <body> 足够高以需要滚动,但为了简洁起见,这部分内容已被隐藏。

html
<div class="anchor" tabindex="0">⚓︎</div>

<div class="infobox">
  <p>Infobox.</p>
</div>

在 CSS 中,我们首先通过给 anchor <div> 一个 anchor-name 来将其声明为锚点元素。定位元素的 position 属性设置为 absolute,并通过其 position-anchor 属性与锚点元素关联。我们还在锚点和信息框上设置了绝对的 heightwidth 尺寸,并在锚点上包含了一个 transition,以便在状态改变时宽度变化能够平滑地动画化:

css
.anchor {
  anchor-name: --my-anchor;
  width: 100px;
  height: 100px;
  transition: 1s all;
}

.infobox {
  position-anchor: --my-anchor;
  position: absolute;
  height: 100px;
  width: 100px;
}

现在来看最有趣的部分。在这里,我们设置了当锚点被悬停或聚焦时,其 width300px。然后我们设置了信息框的:

  • top 值为 anchor(top)。这使得信息框的顶部始终与锚点的顶部保持在一条线上。
  • left 值为 anchor-size(width)。这使得信息框的左侧距离其最近的已定位祖先的左边缘有指定的距离。在这种情况下,指定的距离等于锚点元素的宽度,而最近的已定位祖先是 <body> 元素,所以信息框出现在锚点的右侧。
  • margin-left 值为 calc(anchor-size(width)/4)。这使得信息框总有一个左外边距,将其与锚点分开,该外边距等于锚点宽度的四分之一。
css
.anchor:hover,
.anchor:focus {
  width: 300px;
}

.infobox {
  top: anchor(top);
  left: anchor-size(width);
  margin-left: calc(anchor-size(width) / 4);
}

渲染结果如下:

尝试用 Tab 键聚焦到锚点或用鼠标指针悬停在它上面,注意信息框的位置和左外边距是如何随着锚点元素宽度的增加而成比例增长的。

另见