在很多场景下,Tab 的“选中态”如果只靠文字变粗或颜色变化,给用户的反馈会比较弱,交互性一般。如果有一块会跟随选项移动的背景滑块,就可以显著提升可感知的交互质量,看起来更丝滑更爽。
所谓 Tab 背景切换“丝滑移动”,直觉上像是背景在从 A 选项滑到 B 选项;但实现层面最稳的思路不是改背景图,而是:在 Tab 容器里放一个“滑块元素”(选中背景)滑块做绝对定位,放在各个 Tab 文本的下方,点击/自动切换时,只更新滑块的 left 和 width并使用CSS 的 transition 属性做过渡动画。这样做有几个好处是:DOM 结构清晰,动的永远是同一个滑块、宽度可自适应内容(某些 Tab选项卡宽度更大,所以要注意滑块背景的宽度不要写死,我在这方面踩过很多次坑了o(╥﹏╥)o)。
需要注意的是:滑块必须在“同一个容器”里移动,html示例代码如下:
<div class="tab">
<div class="tab-slide"></div>
<div class="tab-item active">广告创建</div>
<div class="tab-item">广告管理</div>
<div class="tab-item">创意管理</div>
<div class="tab-item">数据报表</div>
</div>
关键点有两条:tab 需要 position:relative,滑块才能用它当参照系;tab-slide 要放在 tab-item 前面或后面都行,但必须用 z-index 控制:滑块在下,文字在上.
接下来用css做过渡动画,css代码示例如下:
.tab {
position: relative;
display: flex;
padding: 8px;
border-radius: 999px;
background: #f4faff;
}
.tab-item {
position: relative;
z-index: 2;
padding: 14px 20px;
border-radius: 999px;
cursor: pointer;
}
.tab-slide {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 10px;
height: 46px;
width: 100px;
border-radius: 999px;
z-index: 1;
transition: all 0.3s ease-in-out;
}
注意的点:滑块用绝对定位让垂直居中更稳,不依赖具体行高。滑块默认 left/width 给一个初值,避免首次渲染闪动。
接下来用用js计算offsetLeft / offsetWidth 目标位置与宽度,示例代码如下:
const tabItems = document.querySelectorAll(".tab-item");
const slide = document.querySelector(".tab-slide");
let index = 0;
function moveSlideTo(item) {
const targetLeft = item.offsetLeft;
const targetWidth = item.offsetWidth;
slide.style.left = `${targetLeft}px`;
slide.style.width = `${targetWidth}px`;
}
tabItems.forEach((item, i) => {
item.addEventListener("click", () => {
index = i;
tabItems.forEach((el) => el.classList.toggle("active", el === item));
moveSlideTo(item);
});
});
// 初始化:让滑块对齐默认 active
const initActive = document.querySelector(".tab-item.active") || tabItems[0];
moveSlideTo(initActive);
注意为什么这套写法“看起来很丝滑”?因为 JS 只是把滑块从旧位置改到新位置,本质是一次样式更新,真正的平滑感来自 CSS 的过渡。
接下来可以按需求加入轮播功能,实现自动轮播。
如果 Tab 对应的内容区也在自动轮播(例如每 3 秒切一个面板),滑块应该同步移动。常见做法:让一个 index 作为状态源记录当前的索引
setInterval 周期性 index++,
点击时清掉定时器,执行切换,再重启定时器(避免用户点击后马上被自动轮播打断,小细节但是很关键)
let timer = null;
const interval = 3000;
function goTo(i) {
index = (i + tabItems.length) % tabItems.length;
const item = tabItems[index];
tabItems.forEach((el) => el.classList.toggle("active", el === item));
moveSlideTo(item);
}
function autoStart() {
timer = setInterval(() => goTo(index + 1), interval);
}
tabItems.forEach((item, i) => {
item.addEventListener("click", () => {
clearInterval(timer);
goTo(i);
autoStart();
});
});
goTo(0);
autoStart();
注意点击时清掉定时器,如果不这么做,用户很有可能在刚点击后又被“自动切走”,会产生“页面不听话”的感觉,用户体验大打折扣。
总结来说很多看似复杂的交互实际上并没有想象的难,比如这个滑块丝滑切换移动,动的只有一个元素,状态由一个索引统一驱动,获取容器的的宽高,让滑块根据容器宽高移动,动画交给 CSS,就这么简单,会了很容易就能写出来,所以说不要被某些看似复杂的或者没见过没写过的交互吓到,仔细想想其实也不是都是这么难哈哈。














A really good blog and me back again.