纯 CSS 实现导航栏下划线滑动需用 .nav::after 伪元素配合 transform: translateX() 和 transition,避免 border-bottom 无法定位与滑动;必须用绝对定位+相对定位父容器,推荐 :has()(现代浏览器)或手动 nth-child 规则;移动端应优先使用 transform 而非 left 以启用硬件加速,动画时长设为 0.3s、缓动用 ease-out。

用 transition 实现导航栏下划线滑动,关键在伪元素 + 定位切换
纯 CSS 实现下划线跟随当前项滑动,不靠 JS,核心是用 ::after 伪元素画线,再通过 transform: translateX() 或 left 移动它,配合 transition 做缓动。直接写 width 或 background-position 动画会卡顿或无法精准对齐。
为什么不能直接给 a 加 border-bottom 并 transition?
因为 border-bottom 是静态样式,无法独立定位到某个菜单项下方;若强行给每个 a 单独设 border-bottom,hover 时只能“出现/消失”,做不到平滑滑动到目标位置。真正可 transition 的必须是同一个 DOM 节点(比如一个共享的 ::after),且它的 left / transform 值能随不同 a 的位置动态计算。
怎么让下划线精准滑到当前 hover 的菜单项下方?
需要结合 position: relative 和 position: absolute 定位:
- 父容器(如
.nav)设position: relative - 每个
a设position: relative(为后续获取 offsetLeft 做准备,但纯 CSS 不需要 JS,所以改用transform+calc()配合 CSS 变量更可控) - 伪元素
.nav::after设position: absolute,bottom: 0,宽度固定(或用width: 100%),再用transform: translateX()移动
实际推荐用 CSS 自定义属性 + :has()(现代浏览器)或逐个写 hover 规则(兼容性更好):
立即学习“前端免费学习笔记(深入)”;
.nav {
position: relative;
display: flex;
}
.nav::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 80px;
height: 3px;
background: #007bff;
border-radius: 2px;
transition: transform 0.3s ease, width 0.3s ease;
}
.nav a {
padding: 12px 20px;
margin: 0 8px;
position: relative;
text-decoration: none;
color: #333;
}
/ 手动为每个 a 设置 hover 时的 translateX /
.nav a:nth-child(1):hover ~ .nav::after {
transform: translateX(0);
width: 80px;
}
.nav a:nth-child(2):hover ~ .nav::after {
transform: translateX(calc(80px + 16px));
width: 80px;
}
.nav a:nth-child(3):hover ~ .nav::after {
transform: translateX(calc(160px + 32px));
width: 80px;
}
⚠️ 注意:上面的 ~ 选择器无效(::after 是 .nav 的伪元素,不是 a 的兄弟)。正确写法是把伪元素挂到每个 a 上,或用 JS —— 但纯 CSS 更稳的方式是:把 ::after 放在 .nav 内部一个空 span 上,再用 JS 控制;若坚持零 JS,则必须用 :has()(Chrome 105+、Safari 15.4+):
.nav {
position: relative;
}
.nav::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 3px;
background: #007bff;
transition: all 0.3s ease;
}
.nav a:hover::after {
content: '';
position: absolute;
bottom: -3px;
left: 0;
width: 100%;
height: 3px;
background: inherit;
transition: none;
}
但这样每项都画一条线,不是“滑动一条线”。所以最实用的方案仍是:用一个绝对定位的 span.underline,配合 JS 监听 mouseenter 获取 getBoundingClientRect(),设置 left 和 width —— 如果你真不想写 JS,就老老实实写 N 条 hover 规则,用 nth-child 算好像素偏移。
移动端适配和性能要注意什么?
transform 和 opacity 是仅有的能高效硬件加速的属性,left/top 会触发重排(layout),尤其在频繁 hover 时掉帧。所以务必用 transform: translateX() 替代 left;同时避免在 ::after 中使用 box-shadow 或渐变背景,它们在低端设备上动画易糊。
- 动画时间控制在
0.2s–0.35s,太长显得拖沓,太短看不出滑动感 - 用
ease-out比ease更自然(起始快,结束缓) - 如果导航项宽度不等,别硬写死
width: 80px,改用width: max-content+transform: scaleX()配合origin
真正难的不是写出来,而是让那条线在各种字号、缩放、RTL 页面里都对得准——这往往需要加 transform: translateZ(0) 强制 GPU 渲染,或用 will-change: transform 提前告知浏览器。










