搞网站导航栏的时候,经常想要一个丝滑的滑动指示条,鼠标滑到哪个菜单项,小蓝条就跟到哪,而且宽度还得自动适应文字长短。以前实现这玩意儿得写一堆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会让移动过程超出目标位置再弹回来,像真的跳过去一样。想要细拆代码的话,可以扒拉扒拉完整示例,里面的时间延迟和形变参数调一调能玩出各种花活。
