凛ノブログ

Eat, Sleep & Daydream

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 似乎还有点问题。

Data on support for the {feature} feature across the major browsers from caniuse.com

本作品采用知识共享署名-非商业性使用-相同方式共享 (CC BY-NC-SA) 协议进行许可。
由于是静态页面,评论提交后不会立即显示,这里 查看提交的评论。