CSS 与布局 / CSS Anchor Positioning · 把元素钉在另一个元素旁 / 用 anchor() 在两个元素之间画连接线 待审核 6 / 7
connections · 串线

用 anchor() 在两个元素之间画连接线

本系列其他各页的浮层都只引用一个锚(anchor() 的基础用法见 anchor() 与 anchor-size() 页)。但 anchor() 里可以写显式锚名,于是同一个元素的四条 inset 能分别锚到不同的锚,把它撑在两个元素之间;只画其中两条边,就得到一根随内容自适应的连接线。这一用法出自 Roland Franke 的 frontend story:在评论与它的回复之间画一根 thread 线,不用 JS,也不做额外测量。

当前浏览器对 anchor positioning 的实测支持情况,由 CSS.supports() 探测。

1 · 四边分别锚定的盒子

一个绝对定位元素,inset-block-start: anchor(--A end) 把顶边钉到 A 的底,inset-block-end: anchor(--B center) 把底边钉到 B 的中线,四条 inset 各引用一个 anchor,元素就被拉伸成跨越 A 与 B 的盒子。再用 border-inline-startborder-block-end 加圆角只画左下两边,得到肘形串线。

图 1-1 · comment 与 reply 之间由 CSS 撑出的肘形连接线。可拖滑块改回复的缩进与垂直间距、更换文案,观察线随布局重新连接。

浮层只关心自己放在哪里,参照一个锚即可;连接线关心的是自己从哪里到哪里,需要两个锚。anchor() 允许同一元素的不同 inset 引用不同锚名,正好表达一段区间。换内容、换字号、换 RTL 时线都跟着重算,因为它读的全是实时布局值,没有写死的坐标。

2 · anchor-scope 对同名锚的隔离

真实页面里会有一长列评论,每个组件复用同样的类名,锚名同为 --comment--reply。不加隔离时,第二个 thread 的线可能去连第一个 thread 的锚。给每个 thread 加 anchor-scope: --comment, --reply,锚名只在本组件子树内解析。

图 2-1 · 两个并存的 thread 在 anchor-scope 隔离下各连各的锚。
图 2-2 · 图 2-1 所用的 anchor-scope 声明位置与作用范围。

警示 · anchor() 只能引用在布局上先于查询元素、且落在其 anchor-scope 内的锚,所以连接线元素要放在两个气泡之后(本例放在 thread 末尾)。这也是它能稳定连上 comment 与 reply 的前提。同一条约束的完整口径见细节与边界页 §1。

3 · 贝塞尔曲线与箭头的几何

肘形线靠 border 画,只能折直角。要平滑曲线加箭头就得改用 SVG,而撑盒子仍由 anchor() 完成:让一个 <svg> 用与肘形线完全相同的四边 anchor() 跨越 comment 与 reply,再把这个盒子的真实像素尺寸喂给 SVG 的 viewBox,使其 1:1 等比而不被拉伸。

图 3-1 · 由 anchor() 撑出盒子、SVG 负责描点的曲线连接线与箭头。可改布局参数观察曲线与箭头一同重算。
图 3-2 · 图 3-1 的 viewBox 换算与箭头坐标计算。

建议 · 箭头与曲线尾端的衔接不必靠目测。取曲线终点 EE 与该处的单位切线 u\vec u,沿切线从 EE 回退箭头长 AA 得到底边中点 BB;描边只画到 BB,箭头三角再从 BB 沿法线取 ±半宽、向 EE 收成尖端。线尾正好落在三角最宽的底边处,盒子被 anchor 撑成任何比例,箭头都覆盖线尾而不突出。前提是 SVG 保持 1:1 不被拉伸,即把 anchor() 撑出的真实像素喂给 viewBox 并在 ResizeObserver 触发时重画,否则非等比缩放会让切线角度失真。