Sliding Tabs
An ink bar under the tabs glides to the active one
Live preview (drag the parameters, watch it change)
Code
<div class="bar" id="bar"><button class="t on">概览</button><button class="t">数据</button><span class="ink" id="ink"></span></div>
<script>
const bar = document.getElementById('bar'), ink = document.getElementById('ink');
function move(el) { ink.style.left = el.offsetLeft + 'px'; ink.style.width = el.offsetWidth + 'px'; }
bar.querySelectorAll('.t').forEach(b => b.onclick = () => {
bar.querySelector('.on').classList.remove('on'); b.classList.add('on'); move(b); });
move(bar.querySelector('.on'));
</script>
<style>
.bar { position: relative; display: flex; gap: 26px; }
.t { padding: 8px 2px; color: #8A8781; background: none; border: none; cursor: pointer; font-size: 15px; }
.t.on { color: #141414; font-weight: 700; }
.ink { position: absolute; bottom: 0; height: 2.5px; background: #141414; border-radius: 99px;
transition: all .4s cubic-bezier(.22,1,.36,1); }
</style> <!-- 墨条的位置由 JS 测量 offsetLeft/offsetWidth 后写 style(同 html-css 档脚本);类名: -->
<div class="relative flex gap-[26px]"><button class="px-0.5 py-2 font-bold text-neutral-900">概览</button><span class="absolute bottom-0 h-[2.5px] rounded-full bg-neutral-900 transition-all duration-300"></span></div> export function SlidingTabs({ tabs }) {
const [cur, setCur] = React.useState(0);
const barRef = React.useRef(null), inkRef = React.useRef(null);
const move = (el) => { if (el && inkRef.current) { inkRef.current.style.left = el.offsetLeft + "px"; inkRef.current.style.width = el.offsetWidth + "px"; } };
React.useEffect(() => { move(barRef.current?.querySelectorAll("button")[cur]); }, [cur]);
return (<div ref={barRef} style={{ position: "relative", display: "flex", gap: 26 }}>
{tabs.map((t, i) => (<button key={i} onClick={() => setCur(i)} style={{ padding: "8px 2px", background: "none", border: "none", cursor: "pointer", fontSize: 15, color: i === cur ? "#141414" : "#8A8781", fontWeight: i === cur ? 700 : 400 }}>{t}</button>))}
<span ref={inkRef} style={{ position: "absolute", bottom: 0, height: 2.5, background: "#141414", borderRadius: 99, transition: "all .4s cubic-bezier(.22,1,.36,1)" }} />
</div>);
} Code snippets are MIT-licensed — copy and adapt freely.
How this effect works
它是怎么工作的(中文)
墨线是绝对定位的小条,切换时 JS 量一下目标标签的 offsetLeft 和 offsetWidth 写给它,CSS 过渡负责「滑」。字体加载会改变文字宽度,记得在字体就绪和窗口 resize 时重新量一次。用 transform 的版本性能更好,但量位置这件事躲不掉。
How it works (English)
The ink bar is an absolutely-positioned strip; on switch, JS measures the target tab's offsetLeft and offsetWidth and writes them over, while a CSS transition does the gliding. Font loading changes text widths — re-measure on font readiness and window resize. A transform-based version performs better, but measuring is unavoidable either way.
参考与来源 Sources
本条为「批次 1(重建)」成果(2026-11-10 用户决定:原 妞妞老师 素材不导出,改由公开材料重建):效果概念参考开源效果生态——Magic UI(MIT)、Uiverse(MIT)、Animista、Paper Shaders(Apache-2.0)、tsParticles(MIT)、CSS Loaders(MIT)等(见 /resources 对应条目,许可以各项目为准);本站的实现代码、参数体系与全部文案均为 Aglaia 原创,未复制上述项目的代码或文案,也与 妞妞老师 原始素材无关。