鼠标滑过导航菜单,那个滑动的小蓝条怎么用CSS锚点定位实现?

3,311字
14–21 分钟
in

搞网站导航栏的时候,经常想要一个丝滑的滑动指示条,鼠标滑到哪个菜单项,小蓝条就跟到哪,而且宽度还得自动适应文字长短。以前实现这玩意儿得写一堆JS算位置,现在CSS出了个锚点定位新特性,几行代码就能搞定。下面直接开干,从零撸一个这样的悬停滑动效果。

目录

锚点定位是个啥

CSS锚点定位说白了就是让一个元素像船一样,锚定在另一个元素边上。被锚定的叫“锚点元素”,跟着跑的叫“目标元素”。定义锚点用anchor-name属性,值必须是以两个短横线开头的名字,比如--li。目标元素通过position-anchor或者anchor()函数来绑定这个锚点。举个栗子:

.锚点 {
  anchor-name: --demo锚;
}
.目标 {
  position: absolute;
  position-anchor: --demo锚;
  left: anchor(left);
  bottom: anchor(bottom);
}

这样目标元素的左边就对齐锚点元素的左边,底部对齐锚点元素的底部。这玩意儿用来做tooltip、菜单滑动条简直绝绝子。

动手搞基础结构

先整一个导航栏的HTML骨架。用nav包一个无序列表,里面塞几个链接:

<nav>
  <ul>
    <li><a href="#">首页</a></li>
    <li class="active"><a href="#">关于</a></li>
    <li><a href="#">项目</a></li>
    <li><a href="#">博客</a></li>
    <li><a href="#">联系</a></li>
  </ul>
</nav>

这里给“关于”菜单加了个active类,表示当前页面。CSS部分先把列表默认样式干掉,再用Flexbox排成一行:

ul {
  padding: 0;
  margin: 0;
  list-style: none;
  display: flex;
  gap: 0.5rem;
  font-size: 2.2rem;
}
ul li a {
  color: #000;
  text-decoration: none;
  font-weight: 900;
  line-height: 1.5;
  padding-inline: 0.2em;
  display: block;
}

这里padding-inline是左右内边距,让文字周围有点呼吸空间。现在导航栏长这样:一排大字横着摆,没有花里胡哨。

背景动画咋整

想要的效果是:鼠标滑过菜单时,从底部“长”出来一个蓝色条。但仔细看会发现,其实是每个菜单项自己的背景在变高。用conic-gradient画一个单色渐变当背景:

ul li {
  background: conic-gradient(lightblue 0 0) bottom / 100% 0% no-repeat;
  transition: 0.2s;
}

这行代码意思是:渐变放在底部,宽度100%,高度0%,不重复。conic-gradient(lightblue 0 0)这种写法虽然看着怪,但它是生成纯色背景最短的姿势。当鼠标滑过或者带有active类的菜单项时,把背景高度拉到100%:

ul li:is(:hover, .active) {
  background-size: 100% 100%;
  transition: 0.2s 0.2s;
}

注意transition里多了个0.2s的延迟,这是为了等会配合滑动条,让背景先缩回去再长出来。还有一个边缘情况:如果鼠标滑到其他菜单项,当前active的那个菜单需要把背景缩回去。用:has选择器搞定:

ul:has(li:hover) li.active:not(:hover) {
  background-size: 100% 0%;
  transition: 0.2s;
}

ul里面有某个li被hover时,把没有被hover的.active菜单背景高度变成0%。这样背景动画就齐活了。现在滑过菜单,蓝色条会从底部冒出来,但它是每个菜单各自为政,不会滑动。

锚点滑动条

滑动效果需要一个公共的红色条跑来跑去。用ul的伪元素::before来当这个滑动条。先把它设成绝对定位,再用锚点定位绑到当前hover的菜单上:

ul::before {
  content: "";
  position: absolute;
  position-anchor: --当前锚点;
  background: red;
  transition: 0.2s;
}

然后给菜单项定义锚点。鼠标滑过或者带active类的菜单,给它起个锚点名叫--当前锚点

ul li:is(:hover, .active) {
  anchor-name: --当前锚点;
}

同样要处理active菜单的锚点移除逻辑——当滑到别的菜单时,原来active的菜单不能再当锚点:

ul:has(li:hover) li.active:not(:hover) {
  anchor-name: none;
}

现在伪元素已经知道要跟着哪个锚点跑了,但还没给尺寸,所以看不见。加上位置和高度:

ul::before {
  bottom: anchor(bottom);
  left: anchor(left);
  right: anchor(right);
  height: 0.2em;
}

anchor(bottom)会取锚点元素底边的位置,anchor(left)取左边,anchor(right)取右边。这样伪元素的左右就自动撑开跟锚点文字一样宽,底部贴在锚点底部。滑过不同菜单时,锚点变化,伪元素的位置和宽度会跟着变,再加上transition平滑过渡,滑动条效果就出来了。也可以把position-anchor省略,直接用anchor()函数带上锚点名:

ul::before {
  inset: auto anchor(--当前锚点 right) anchor(--当前锚点 bottom) anchor(--当前锚点 left);
  height: 0.2em;
}

这种写法适合一个目标元素绑多个不同锚点的场景。现在滑动条能丝滑地左右横跳了。

组合两个动画

把背景动画和滑动条动画拼在一起,注意过渡时间要错开。整个效果分三步:背景缩回去 → 滑动条移动 → 背景长出来。所以延迟设置如下:

ul::before {
  transition: 0.2s 0.2s;  /* 滑动条晚0.2秒开始动 */
}
ul li {
  transition: 0.2s;       /* 背景缩回立即开始 */
}
ul li:is(:hover, .active) {
  transition: 0.2s 0.4s;  /* 背景长出来晚0.4秒 */
}
ul:has(li:hover) li.active:not(:hover) {
  transition: 0.2s;       /* active项缩回立即开始 */
}

滑过菜单时:

  • 原来active菜单的背景先缩回(花0.2秒)
  • 滑动条等0.2秒后开始移动到新菜单(花0.2秒)
  • 新菜单的背景再等0.4秒后长出来(花0.2秒)

完美衔接,视觉上就像同一个条子在滑。下面这个表格总结了每个动画的时间点:

阶段耗时延迟
背景缩回0.2秒0秒
滑动条移动0.2秒0.2秒
背景长出0.2秒0.4秒

弹跳小圆点

换一种玩法,不搞滑动条,搞一个弹跳的小圆点。鼠标滑过菜单时,蓝色矩形先缩成一个圆,然后“砰”地弹到下一个菜单,再展开成矩形。实现思路跟上面类似,还是两个动画叠加。每个菜单自己的伪元素负责矩形变圆的形变:

ul li::before {
  content: "";
  position: absolute;
  inset: 0;
  background: lightblue;
  border-radius: 0;
  transition: 0.3s;
  scale: 1 1;
}
ul li:is(:hover, .active)::before {
  border-radius: 50%;
  scale: 0.2 0.2;
}

同时ul的伪元素做成一个小圆点,用锚点定位在不同菜单之间跳转,跳转时的缓动函数用自定义的cubic-bezier制造弹跳感:

ul::before {
  transition: 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

这个cubic-bezier会让移动过程超出目标位置再弹回来,像真的跳过去一样。想要细拆代码的话,可以扒拉扒拉完整示例,里面的时间延迟和形变参数调一调能玩出各种花活。