CSS锚点定位指南

了解 CSS 锚点定位,包括其语法、属性、如何将一个元素定位在另一个元素旁边,甚至如何根据其他元素调整元素大小。

不久前,若想让工具提示或弹出框置于其他元素上方,我们必须将工具提示的位置属性设为非static值,并通过inset/transform属性 精确 定位。这种方法可行,但元素位置易受用户滚动、缩放或动画影响——工具提示可能溢出屏幕或出现在尴尬位置。唯一的解决方式是通过 JavaScript 实时检测提示框是否超出边界,然后再次借助 JavaScript 进行修正。

CSS锚点定位功能为我们提供了一个简洁的接口,只需在CSS中指定连接方向,即可将元素紧贴其他元素排列。它还允许设置备用位置,从而避免前文所述的溢出问题。例如,我们可以将工具提示元素置于锚点上方,当上方空间不足时,允许其折叠至锚点下方显示。

锚定位功能在浏览器支持速度上与其他特性截然不同: 其首个草案发布于2023年6月,仅一年后便随Chrome 125版本上线。作为对比,CSS变量规范的初稿发布于2012年,但耗时四年才获得广泛浏览器支持。

接下来我们将深入探讨锚点元素的实现细节,包括如何关联目标元素、定位及尺寸控制等。

快速参考

/* Define an anchor element */
.anchor {
  anchor-name: --my-anchor;
}
/* Anchor a target element */
.target {
  position: absolute;
  position-anchor: --my-anchor;
}
/* Position a target element */
.target { 
  position-area: start end;
}

基础概念与术语

CSS锚点定位最基础的功能,是引入了一种全新的页面元素相对定位方式。为便于理解,我们将使用特定名称明确各元素间的关联关系:

  • 锚点: 作为定位其他元素的参照点,故称之为 锚点
  • 目标元素: 相对于一个或多个锚点定位的绝对定位元素。后续我们将统一使用 目标元素 这一名称,但规范中常简称为“绝对定位元素”。

在后续代码示例和演示中,可将它们视为并排放置的两个<div>元素。

<div class="anchor">anchor</div>
<div class="target">target</div>

CSS锚点定位的核心在于绝对定位元素(即position: absolute),因此在深入探讨前需回顾若干基础概念。

  • 包含块: 这是包含元素的框。对于绝对定位元素,其包含块为最近祖先元素中具有非static定位方式或具有containfilter等特定属性值的视口。
  • 内边距修饰容器块(IMCB): 对于绝对定位元素,内边距属性(toprightbottomleft等)会缩小其定位和尺寸计算的容器块范围,从而形成新的容器块——即内边距修饰容器块(简称IMCB)。此概念至关重要,因为本指南涉及的属性(如position-areaposition-try-order)均依赖于此原理。

将目标元素绑定至锚点

首先解析确定锚点定位的两个属性:anchor-name用于定义锚点元素,position-anchor则将目标元素绑定至锚点元素。

Square labeled as "anchor"

anchor-name

普通元素默认并非锚点——我们需要显式将其设为锚点。最常见的方式是赋予名称,可通过anchor-name属性实现。

anchor-name: none | <dashed-ident>#

名称必须是 <dashed-ident>,即以两个连字符(--)开头的自定义名称,例如 --my-anchor--MyAnchor

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

这便构成了锚点元素。它需要关联一个目标元素,即通过position-anchor属性设置的“目标”元素。

Square labeled as "target"

position-anchor

目标元素是具有绝对定位的元素,其定位与锚点元素通过anchor-name属性声明的名称关联。此属性将目标元素绑定至锚点元素。

position-anchor: auto | <anchor-element>

该属性需接收有效的<anchor-element>。因此若将其他元素设为“锚点”,可通过position-anchor属性设置目标元素:

.target {
  position: absolute;
  position-anchor: --my-anchor;
}

通常若未找到有效锚点元素,其他锚点属性与函数将被忽略。

定位目标

既然已掌握建立锚点-目标关系的机制,接下来可针对目标元素相对于锚点元素的 定位 进行设置。以下两项属性用于控制目标元素在锚点元素两侧的定位位置(position-area)以及当空间不足时隐藏目标元素的条件(position-visibility)。

Anchor element with target elements spanning around it.

position-area

下一步是将目标相对于其锚点定位。最简单的方法是使用position-area属性,该属性会在锚点元素周围创建一个虚拟的3×3网格,让我们能够将目标放置在网格的一个或多个区域中。

position-area: auto | <position-area>

其工作原理是通过设置网格的行和列来实现,使用 逻辑值startend(取决于书写模式);物理值topleftrightbottom以及共享值center,随后将目标元素的内部可视边界(IMCB)缩小至所选网格区域。

.target {
  position-area: top right;
  /* or */
  position-area: start end;
}

逻辑值参照包含块的书写模式,但若需相对于目标本身的书写模式定位,需在值前添加self前缀。

.target {
  position-area: self-start self-end;
}

所有轴均可使用center值。

.target {
  position-area: center right;
  /* or */
  position-area: start center;
}

若需将目标跨两个相邻网格区域放置,可在任意值(非center)前添加span-前缀,每次仅限横向或纵向扩展。

.target {
  position-area: span-top left;
  /* or */
  position-area: span-start start;
}

最后,使用span-all值可使目标跨三个相邻网格区域延伸。

.target {
  position-area: bottom span-all;
  /* or */
  position-area: end span-all;
}

您可能注意到position-area属性的物理值顺序并不严格:position-area: top leftposition-area: left top效果相同,但逻辑值的顺序至关重要——position-area: start endposition-area: end start的含义完全相反。

我们可以通过在逻辑值前添加所需轴前缀(如y-x-inline-block-)使其互换使用。

.target {
  position-area: inline-end block-start;
  /* or */
  position-area: y-start x-end;
}

Examples on each position-visibility value: always showing the target, anchors-visible hiding it when the anchor goes out of screen and no-overflow hiding it when the target overflows

position-visibility

该属性提供特定条件以隐藏目标元素于视口之外。

position-visibility: always | anchors-visible | no-overflow
  • always: 目标元素始终显示,不受其锚点或溢出状态影响。
  • no-overflow: 若应用定位回退后目标元素仍溢出其包含块,则强制隐藏。
  • anchors-visible:锚点(而非目标元素)完全溢出其包含块或被其他元素完全覆盖时,目标元素将被强制隐藏。
position-visibility: always | anchors-visible | no-overflow

设置备用位置

当目标元素定位于锚点后,可为其添加额外指令,规定空间不足时的处理方式。我们已通过position-visibility属性实现过隐藏效果。但以下两个属性能更精细地控制目标元素的 重新定位:通过尝试锚点的其他边界(position-try-fallbacks)以及重新定位的尝试顺序(position-try-order)。

这两个属性可通过position-try简写属性同时声明——在了解两个组成属性后我们将对此进行说明。

Examples on each try-tactic: flip-block flipping the target from the top to the bottom, flip-inline from left to right and flip-start from left to top (single value) and top right to left bottom (two values)

position-try-fallbacks

该属性接受以逗号分隔的 位置备用方案 列表,当目标元素在包含块中溢出空间时将依次尝试这些方案。属性会逐个使用备用值重新定位,直至找到合适方案或耗尽所有选项。

position-try-fallbacks: none | [ [<dashed-ident> || <try-tactic>] | <'inset-area'>  ]#
  • none: 保持目标位置选项列表为空。
  • <dashed-ident>: 在选项列表中添加名为给定值的自定义@position-try备用方案。若不存在匹配的@position-try,则忽略该值。
  • <尝试策略>: 通过在三个轴向之一翻转目标当前位置创建选项列表,每个轴向由独立关键词定义。可组合使用以叠加效果。
    • flip-block 关键词交换块轴方向的值。
    • flip-inline 关键词交换行内轴方向的值。
    • flip-start 关键词对对角线方向的值进行交换。
  • <dashed-ident> || <try-tactic>: 结合自定义 @try-option<try-tactic> 创建单位置回退方案。<try-tactic> 关键词也可组合以叠加效果。
  • <“位置区域”> 使用 位置区域 语法将锚点移动至新位置。
.target {
  position-try-fallbacks:
    --my-custom-position,
    --my-custom-position flip-inline,
    bottom left;
}

two targets sorrounding an anchor, positioned where the IMCB is the largest.

position-try-order

该属性根据哪个位置能为目标提供最大空间,从position-try-fallbacks属性中定义的备用值中选择新位置。其余选项按可用空间从大到小重新排序。

position-try-order: normal | most-width | most-height | most-block-size | most-inline-size

“更多空间”具体指什么?针对每个位置备选方案,它会计算目标元素的IMCB尺寸。然后根据选定的选项,选择能使IMCB获得最大宽度或高度的值:

  • most-width
  • most-height
  • most-block-size
  • most-inline-size
.target {
  position-try-fallbacks: --custom-position, flip-start;
  position-try-order: most-width;
}

position-try

此为简写属性,将position-try-fallbacksposition-try-order属性合并为单一声明。它先接受排序顺序,再接受可能的位置备选方案列表。

position-try: < "position-try-order" >? < "position-try-fallbacks" >;

因此,我们可以将两个属性合并为单一样式规则:

.target {
  position-try: most-width --my-custom-position, flip-inline, bottom left;
}

自定义位置与尺寸备用方案

@position-try

此@规则为position-try-fallbacks属性定义自定义位置备用方案。

@position-try <dashed-ident> {
  <declaration-list>
}

它接受多种用于改变目标元素位置与尺寸的属性,并将它们组合为该元素尝试的新位置备用方案。

设想这样一个场景:你已建立锚点与目标元素的关联关系。若想将目标元素定位在锚点的右上角,只需使用之前介绍的position-area属性即可轻松实现:

.target {
  position: absolute;
  position-area: top right;
  width: 100px;
}

注意到.target元素被设置为100px大小了吗?在某些屏幕上可能因空间不足,导致它无法继续显示在锚点的右上角。此时我们可以为.target提供之前提到的备用方案,使其尝试重新定位到空间更充足的边缘:

.target {
  position: absolute;
  position-area: top right;
  position-try-fallbacks: top left;
  position-try-order: most-width;
  width: 100px;
}

作为追求代码优雅的优秀CSS开发者,不妨将这两个属性合并为position-try简写属性:

.target {
  position: absolute;
  position-area: top right;
  position-try: most-width, flip-inline, bottom left;
  width: 100px;
}

目前效果良好。锚定目标元素初始定位于锚点的右上角(100px处)。若该位置空间不足,则根据position-try属性决定将目标重新定位至锚点的左上角(声明为flip-inline)或左下角——选择能提供最大宽度的边界。

但若需在重新定位时同步 调整目标元素尺寸 呢?比如目标元素在两个备用位置的100px显示宽度都过大,需要改为50px。此时可通过@position-try实现精准控制:

@position-try --my-custom-position {
  position-area: top left;
  width: 50px;
}

完成上述操作后,我们现在拥有了一个名为--my-custom-position的自定义属性,可用于position-try简写属性。在此情况下,@position-try可替代flip-inline值,因为它等同于top left

@position-try --my-custom-position {
  position-area: top left;
  width: 50px;
}

.target {
  position: absolute;
  position-area: top right;
  position-try: most-width, --my-custom-position, bottom left;
  width: 100px;
}

这样,当.target元素尝试重新定位到锚点的右上角时,其宽度会从100px调整为50px。这种灵活性使我们更有可能在任何布局中实现元素的完美契合。

锚点函数

anchor()

可将 CSS 的 anchor() 函数视为将目标元素绑定至锚点的快捷方式——只需一次指定锚点元素、目标元素的定位方向及尺寸大小。但正如后续将展示的,该函数还支持将单个目标元素绑定至多个锚点元素。

此函数的正式语法最多可接受三个参数:

anchor( <anchor-element>? && <anchor-side>, <length-percentage>? )

因此,我们需要指定锚点元素、目标元素的定位方向以及目标元素的尺寸。值得注意的是,anchor() 仅能应用于内边距相关属性(如 topleftinset-block-end 等)。

.target {
  top: anchor(--my-anchor bottom);
  left: anchor(--my-anchor end, 50%);
}

让我们解析该函数的参数:

<anchor-element>

此参数指定要将目标元素附加到的锚点元素。可通过锚点名称进行指定(参见“将目标附加到锚点”)。

也可选择完全不指定锚点。此时目标元素将使用position-anchor中定义的隐式锚点元素。若不存在隐式锚点,函数将解析为其备用方案;否则该参数无效且被忽略。

<anchor-side>

此参数用于设定目标元素在锚点位置的对齐边界,例如锚点的topleftbottomright等。

但实际选项更为丰富,包括逻辑侧边关键词(insideoutside)、基于用户书写模式的逻辑方向参数(startendself-startself-end)以及经典的center

  • <anchor-side>:解析为锚点元素对应侧面的<length>值。包含物理属性(topleftbottomright)、逻辑侧面属性(insideoutside)、基于用户书写模式的逻辑方向属性(startendself-startself-end)以及center属性。
  • <百分比>:表示在起始点(0%)与终点(100%)之间的位置。允许使用小于0%和大于100%的值。

<长度-百分比>

此参数完全可选,因此您可根据需要省略。若使用该参数,当目标元素缺乏有效锚点或定位时,它将自动调整目标元素的尺寸。该参数会将目标元素定位到相对于其包含块的固定<length><percentage>位置。

让我们通过不同参数类型的示例进行说明,因为它们的效果存在细微差异。

使用物理参数

targets sorrounding the anchor. each with a different position

物理参数(toprightbottomleft)可用于定位目标元素,不受用户书写模式影响。例如,可将目标元素的rightbottom内边距属性定位于锚点的anchor(top)anchor(left)边界,从而将目标元素置于锚点的左上角:

.target {
  bottom: anchor(top);
  right: anchor(left);
}

使用逻辑边界关键词

targets sorrounding the anchor. each with a different position

逻辑边界参数(即insideoutside)取决于其所在的内边距属性。inside参数将选择与内边距属性相同的侧边,而outside参数则选择相反侧边。例如:

.target {
  left: anchor(outside);
  /* is the same as */
  left: anchor(right);

  top: anchor(inside);
  /* is the same as */
  top: anchor(top);
}

使用逻辑方向参数

targets sorrounding the anchor. each with a different position

逻辑方向参数 取决于两个因素:

  1. 用户的书写模式:可遵循包含块的书写模式(startend),或目标元素自身的书写模式(self-startself-end)。
  2. 它们所使用的内边距属性:它们将选择与内边距属性相同的轴。

例如,在从左到右的水平书写模式中使用物理内边距属性时,效果如下:

.target {
  left: anchor(start);
  /* is the same as */
  left: anchor(left);

  top: anchor(end);
  /* is the same as */
  top: anchor(bottom);
}

而在从右到左的书写模式中,则应这样设置:

.target {
  left: anchor(start);
  /* is the same as */
  left: anchor(right);

  top: anchor(end);
  /* is the same as */
  top: anchor(bottom);
}

这可能很快令人困惑,因此我们还应使用逻辑参数配合逻辑内边距属性,以确保书写模式得到优先遵循:

.target {
  inset-inline-start: anchor(end);
  inset-block-start: anchor(end);
}

使用百分比值

targets sorrounding the anchor. each with a different position

百分比值 可用于将目标定位在start(0%)与end(100%)之间的任意位置。由于百分比值相对用户书写模式而定,建议配合逻辑内边距属性使用。

.target {
  inset-inline-start: anchor(100%);
  /* is the same as */
  inset-inline-start: anchor(end);

  inset-block-end: anchor(0%);
  /* is the same as */
  inset-block-end: anchor(start);
}

系统接受小于0%和大于100%的数值,因此-100%将目标向起始端移动,200%则向终止端移动。

.target {
  inset-inline-start: anchor(200%);
  inset-block-end: anchor(-100%);
}

使用中心关键词

targets sorrounding the anchor. each with a different position

center参数 等效于50%。可以说它对方向属性具有“免疫性”,因此无论与物理内边距还是逻辑内边距属性配合使用均无问题。

.target {
  position: absolute;
  position-anchor: --my-anchor;

  left: anchor(center);
  bottom: anchor(top);
}

anchor-size()

anchor-size() 函数的独特之处在于它能根据锚点元素的大小来调整目标元素的尺寸。这对于确保目标元素随锚点元素缩放至关重要,尤其在响应式设计中——此时元素常因溢出容器而发生位移、尺寸变化或被遮蔽。

该函数接受锚点的边长参数,并将其解析为 <length> 值,实质上返回锚点的 widthheightinline-sizeblock-size

anchor-size( [ <anchor-element> || <anchor-size> ]? , <length-percentage>? )

anchor with an anchor-size() function on each side

以下是 anchor-size() 函数可用的参数:

  • <anchor-size> 指锚元素的边界方向。
  • <长度-百分比> 此可选参数可在目标元素缺乏有效锚点或尺寸时作为备用方案,返回相对于其包含块的固定<长度><百分比>值。

我们还可将该函数声明在目标元素的widthheight属性上,使其随锚点调整尺寸——甚至可同时作用于两者!

.target {
  width: anchor-size(width, 20%); /* uses default anchor */`
  height: anchor-size(--other-anchor inline-size, 100px);
}

多重锚点

在上节中我们学习了anchor()函数。该函数的一个特殊之处在于,它只能声明在基于内边距的属性上,我们所见的所有示例都体现了这一点。这看似限制了函数的应用场景,实则赋予了anchor()超越锚定位属性的超能力:它支持同时声明多个内边距基准属性。 因此我们能够为同一目标元素设置多个锚点函数!

以下是上一节中 anchor() 函数的早期示例之一:

.target {
  top: anchor(--my-anchor bottom);
  left: anchor(--my-anchor end, 50%);
}

我们同时在 topleft 内边距属性上声明了同一个名为 --my-anchor 的锚点元素。但这并非必要做法——我们完全可以将目标元素关联到多个锚点元素。

.anchor-1 { anchor-name: --anchor-1; }
.anchor-2 { anchor-name: --anchor-2; }
.anchor-3 { anchor-name: --anchor-3; }
.anchor-4 { anchor-name: --anchor-4; }

.target {
  position: absolute;
  inset-block-start: anchor(--anchor-1);
  inset-inline-end: anchor(--anchor-2);
  inset-block-end: anchor(--anchor-3);
  inset-inline-start: anchor(--anchor-4);
}

或者更简洁地实现:

.anchor-1 { anchor-name: --anchor-1; }
.anchor-2 { anchor-name: --anchor-2; }
.anchor-3 { anchor-name: --anchor-3; }
.anchor-4 { anchor-name: --anchor-4; }

.target {
  position: absolute;
  inset: anchor(--anchor-1) anchor(--anchor-2) anchor(--anchor-3) anchor(--anchor-4);
}

以下演示展示了将目标元素绑定至两个注册锚点的<textarea>元素。<textarea>支持点击拖拽调整尺寸,这两个元素被绝对定位在页面对角线两端。若将目标元素同时绑定至两个锚点,当调整锚点尺寸时,目标元素会随之在页面上拉扯变形,形成类似两锚点拔河的动态效果。

截至本文撰写时,该演示仅在Chrome浏览器中支持,因此我们附上视频以便您直观了解其运作原理。

无障碍支持

锚点定位最直接的应用场景是创建工具提示、信息框和弹出框,但也可用于装饰性元素。这意味着锚点定位无需在锚点元素与目标元素间建立语义关联。您可能立刻察觉到问题所在:非视觉设备(如屏幕阅读器)无法理解两个看似无关的元素如何关联。

举例来说,假设我们有一个名为.tooltip的元素,将其设置为锚定到另一个名为.anchor元素的目标元素。

<div class="anchor">anchor</div>
<div class="toolip">toolip</div>
.anchor {
  anchor-name: --my-anchor;
}

.toolip {
  position: absolute;
  position-anchor: --my-anchor;
  position-area: top;
}

我们需要在DOM中建立这两个元素的关联,使其共享辅助技术可解析的上下文。使用ARIA属性描述元素的通用准则通常是:不要这样做。 至少在没有其他语义化实现方式时应避免使用。

这正是需要使用ARIA属性的典型场景。在采取任何其他措施之前,屏幕阅读器目前只能识别两个并列元素,而无法感知它们之间的标记关系。这对无障碍访问而言是个遗憾,但我们可通过对应的ARIA属性轻松解决:

<div class="anchor" aria-describedby="tooltipInfo">anchor</div>
<div class="toolip" role="tooltip" id="tooltipInfo">toolip</div>

现在它们在视觉和语义上都紧密关联了!若您对ARIA属性尚不熟悉,建议阅读Adam Silver的《为何、如何及何时使用语义HTML与ARIA》,这是一篇极佳的入门指南。

浏览器支持情况

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
125NoNo12526.0

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
140No14026.0

规范变更

CSS锚点定位自编辑草案发布以来经历了多次变更。Chrome浏览器团队在该特性尚在定义阶段便迅速加入并实现了锚点定位功能,这导致混淆——基于Chromium的浏览器在规范持续修订期间已部分实现了锚点定位功能。

我们将列举浏览器因规范变更而需更新实现的具体案例。虽然略显复杂,但截至Chrome 129+版本,已发布但后续变更的内容如下:

position-area

inset-area属性已更名为position-area(#10209),但Chrome 131版本前仍将支持旧名称。

.target {
  /* from */
  inset-area: top right;

  /* to */
  position-area: top right;
}

位置尝试备用方案

位置尝试选项已重命名为位置尝试备用方案(#10395)。

.target {
  /* from */
  position-try-options: flip-block, --smaller-target;

  /* to */
  position-try-fallbacks: flip-block, --smaller-target;
}

inset-area()

inset-area() 包装函数在 position-try-fallbacks 中已不存在 (#10320),现在可直接编写值而无需包装:

.target {
  /* from */
  position-try-options: inset-area(top left);

  /* to */
  position-try-fallbacks: top left;
}

anchor(center)

最初若需将目标居中定位,需使用繁琐的语法:

.target {
  --center: anchor(--x 50%);
  --half-distance: min(abs(0% - var(--center)), abs(100% - var(--center)));

  left: calc(var(--center) - var(--half-distance));
  right: calc(var(--center) - var(--half-distance));
}

CWSSG工作组已解决(#8979),新增anchor(center)参数,免去繁琐的语法转换:

.target {
  left: anchor(center);
}

已知问题

是的,CSS锚点定位确实存在一些问题,至少在本指南撰写时如此。例如规范规定:若元素未设置默认锚点元素,position-area将失效。此为已知问题(#10500),但仍可复现。

因此以下代码…

.container {
  position: relative;
}

.element {
  position: absolute;
  position-area: center;
  margin: auto;
}

...至少在Chrome中会将.element居中显示于容器内:

感谢Afif13提供的精彩演示!

另一个例子涉及position-visibility属性。当锚点元素超出视区或屏幕范围时,通常希望目标元素也随之隐藏。规范规定该属性的默认值应为anchors-visible,但浏览器实际默认采用always

当前Chrome的实现并未遵循规范,确实将always作为初始值。但规范设计初衷明确:当锚点元素超出屏幕或被滚动隐藏时,通常需要实现隐藏效果。 (#10425)

延伸阅读