Anchor 初体验:实现一个可追随的 Navbar 滑块
我在 blog 上一个 theme 折腾导航栏的时候,为了实现那个“滑块跟随鼠标”的效果,跟 CSS 里的 nth-child 还有 translateX 较劲了半天。
偏移百分比和像素是手动调式出来的。要是按钮里的文字长短不一,那简直灾难。
手算偏移量
这是我以前最常用的办法。给导航栏加个伪元素当背景,然后鼠标指到哪个,就手动把背景“挪”过去。当然这类效果用 JS 其实更好实现,但个人的强迫症,我是不会在导航上引入 JS 的。
代码写起来大概长这样:
NavbarLegacy.astro
<nav
class="navbar-legacy relative flex justify-center gap-1 my-8 p-1 bg-zinc-100/50 dark:bg-zinc-800/50 rounded-full w-fit mx-auto border border-zinc-200 dark:border-zinc-700"
>
<a
href="#"
class="active px-4 py-1.5 text-sm font-medium transition-colors z-10"
>首页</a
>
<a href="#" class="px-4 py-1.5 text-sm font-medium transition-colors z-10"
>归档</a
>
<a href="#" class="px-4 py-1.5 text-sm font-medium transition-colors z-10"
>友链</a
>
<a href="#" class="px-4 py-1.5 text-sm font-medium transition-colors z-10"
>关于</a
>
</nav>
<style>
.navbar-legacy {
position: relative;
}
.navbar-legacy::before {
content: "";
position: absolute;
top: 4px;
left: 4px;
width: 60px;
height: 32px;
border-radius: 9999px;
z-index: 0;
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.2s;
opacity: 0;
@apply bg-white dark:bg-zinc-700 shadow-sm ring-1 ring-zinc-900/5;
}
.navbar-legacy a {
color: #71717a; /* zinc-500 */
text-decoration: none;
}
.navbar-legacy a.active,
.navbar-legacy a:hover {
color: #18181b; /* zinc-900 */
}
:global(.dark) .navbar-legacy a.active,
:global(.dark) .navbar-legacy a:hover {
color: #fafafa; /* zinc-50 */
}
/* 逻辑控制:各种 has 选择器配合手动计算的偏移 */
.navbar-legacy:has(a:nth-child(1).active)::before,
.navbar-legacy:has(a:nth-child(1):hover)::before {
opacity: 1;
transform: translateX(0);
}
.navbar-legacy:has(a:nth-child(2).active)::before,
.navbar-legacy:has(a:nth-child(2):hover)::before {
opacity: 1;
transform: translateX(64px); /* 手动计算的间距 */
}
.navbar-legacy:has(a:nth-child(3).active)::before,
.navbar-legacy:has(a:nth-child(3):hover)::before {
opacity: 1;
transform: translateX(128px);
}
.navbar-legacy:has(a:nth-child(4).active)::before,
.navbar-legacy:has(a:nth-child(4):hover)::before {
opacity: 1;
transform: translateX(192px);
}
</style> 每增加一个导航链接,就得去重新算一遍偏移量。之前导航还有图标 + 响应式设计,手机上没图标就露馅了,为了动效把图标砍了。
Anchor
用上 Anchor Positioning 之后,逻辑变得非常简洁。滑块只需要通过声明式语言指定它的锚点目标即可。
这种方式让定位逻辑回归到了最直觉的状态:你只需要声明“要跟谁对齐”,剩下的适配工作交给浏览器原生处理。
比如下面 .indicator 声明了 position-anchor: --nav-anchor
NavbarAnchor.astro
<nav
class="navbar-anchor relative flex justify-center gap-1 my-8 p-1 bg-zinc-100/50 dark:bg-zinc-800/50 rounded-full w-fit mx-auto border border-zinc-200 dark:border-zinc-700"
>
<a href="#">首页</a>
<a href="#">归档</a>
<a href="#" class="px-6">特别长的友链</a>
<a href="#" class="active">关于</a>
<div class="indicator"></div>
</nav>
<style>
.navbar-anchor a {
padding: 0.375rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #71717a;
text-decoration: none;
z-index: 10;
transition: color 0.2s;
}
.navbar-anchor a:hover,
.navbar-anchor a.active {
color: #18181b;
}
:global(.dark) .navbar-anchor a:hover,
:global(.dark) .navbar-anchor a.active {
color: #fafafa;
}
a.active {
anchor-name: --nav-anchor;
}
a:hover {
anchor-name: --nav-anchor;
}
:has(a:hover:not(.active)) a.active {
anchor-name: none;
}
.indicator {
position: absolute;
position-anchor: --nav-anchor;
left: anchor(left);
right: anchor(right);
top: anchor(top);
bottom: anchor(bottom);
background-color: white;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border-radius: 9999px;
z-index: 0;
transition:
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
:global(.dark) .indicator {
background-color: #3f3f46;
@apply ring-1 ring-white/10;
}
</style> 按钮里的文字不管多长,anchor(right) 都能精准映射到边缘,滑块也因此实现了自动适配。
总结
在以往的方案中,依赖 JavaScript 监听尺寸变化,而现在,我们可以直接使用声明式语言定义元素间的逻辑关联,将复杂的坐标计算交给浏览器的渲染引擎处理。
浏览器支持
最新浏览器已经基本支持了,Firefox 似乎还有点问题。
本作品采用知识共享署名-非商业性使用-相同方式共享 (CC BY-NC-SA) 协议进行许可。