32 CSS Tab Designs01 / 32
Ink Slider
Solid ink-violet bar slides horizontally beneath the active tab. JS measures offsetLeft + offsetWidth so the bar matches each tab's exact width — no fixed-width assumption — and re-aligns on viewport resize.
Published
The code
<div class="tt21">
<nav class="tt21n">
<span class="tt21bar" aria-hidden="true"></span>
<button class="tt21b active" data-t>Overview</button>
<button class="tt21b" data-t>Pricing</button>
<button class="tt21b" data-t>Reviews</button>
<button class="tt21b" data-t>Support</button>
</nav>
</div><div class="tt21">
<nav class="tt21n">
<span class="tt21bar" aria-hidden="true"></span>
<button class="tt21b active" data-t>Overview</button>
<button class="tt21b" data-t>Pricing</button>
<button class="tt21b" data-t>Reviews</button>
<button class="tt21b" data-t>Support</button>
</nav>
</div>.tt21 {
background: #faf8f3;
padding: 32px 18px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt21n {
position: relative;
display: flex;
border-bottom: 1px solid rgba(35, 28, 72, 0.12);
min-width: 0;
}
.tt21bar {
position: absolute;
bottom: -1px;
left: 0;
height: 3px;
width: 0;
background: #231c48;
border-radius: 2px 2px 0 0;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
pointer-events: none;
}
.tt21b {
flex: 1;
min-width: 0;
padding: 12px 14px 14px;
border: 0;
background: transparent;
font: 600 13px/1 ui-sans-serif, system-ui;
color: rgba(35, 28, 72, 0.55);
cursor: pointer;
transition: color 0.25s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt21b:hover {
color: rgba(35, 28, 72, 0.85);
}
.tt21b.active {
color: #231c48;
}
.tt21b:focus-visible {
outline: 2px solid #231c48;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.tt21bar {
transition: none;
}
}.tt21 {
background: #faf8f3;
padding: 32px 18px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt21n {
position: relative;
display: flex;
border-bottom: 1px solid rgba(35, 28, 72, 0.12);
min-width: 0;
}
.tt21bar {
position: absolute;
bottom: -1px;
left: 0;
height: 3px;
width: 0;
background: #231c48;
border-radius: 2px 2px 0 0;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
pointer-events: none;
}
.tt21b {
flex: 1;
min-width: 0;
padding: 12px 14px 14px;
border: 0;
background: transparent;
font: 600 13px/1 ui-sans-serif, system-ui;
color: rgba(35, 28, 72, 0.55);
cursor: pointer;
transition: color 0.25s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt21b:hover {
color: rgba(35, 28, 72, 0.85);
}
.tt21b.active {
color: #231c48;
}
.tt21b:focus-visible {
outline: 2px solid #231c48;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.tt21bar {
transition: none;
}
}/* Ink Slider — toggle .active and slide the ink bar to the active tab.
Re-positions on viewport resize so the bar stays locked to its button. */
(function () {
var nav = document.querySelector(".tt21n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var bar = nav.querySelector(".tt21bar");
var current = null;
function reposition() {
if (!current || !bar) return;
bar.style.left = current.offsetLeft + "px";
bar.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
reposition();
}
btns.forEach(function (b) {
b.addEventListener("click", function () {
activate(b);
});
});
window.addEventListener("resize", reposition);
var initial = nav.querySelector("[data-t].active") || btns[0];
if (initial) activate(initial);
})();/* Ink Slider — toggle .active and slide the ink bar to the active tab.
Re-positions on viewport resize so the bar stays locked to its button. */
(function () {
var nav = document.querySelector(".tt21n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var bar = nav.querySelector(".tt21bar");
var current = null;
function reposition() {
if (!current || !bar) return;
bar.style.left = current.offsetLeft + "px";
bar.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
reposition();
}
btns.forEach(function (b) {
b.addEventListener("click", function () {
activate(b);
});
});
window.addEventListener("resize", reposition);
var initial = nav.querySelector("[data-t].active") || btns[0];
if (initial) activate(initial);
})();Here's a working CSS Tab Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Ink Slider
Source: https://codefronts.com/navigation/css-tabs/ink-slider/
Solid ink-violet bar slides horizontally beneath the active tab. JS measures offsetLeft + offsetWidth so the bar matches each tab's exact width — no fixed-width assumption — and re-aligns on viewport resize.
## HTML
```html
<div class="tt21">
<nav class="tt21n">
<span class="tt21bar" aria-hidden="true"></span>
<button class="tt21b active" data-t>Overview</button>
<button class="tt21b" data-t>Pricing</button>
<button class="tt21b" data-t>Reviews</button>
<button class="tt21b" data-t>Support</button>
</nav>
</div>
```
## CSS
```css
.tt21 {
background: #faf8f3;
padding: 32px 18px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt21n {
position: relative;
display: flex;
border-bottom: 1px solid rgba(35, 28, 72, 0.12);
min-width: 0;
}
.tt21bar {
position: absolute;
bottom: -1px;
left: 0;
height: 3px;
width: 0;
background: #231c48;
border-radius: 2px 2px 0 0;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
pointer-events: none;
}
.tt21b {
flex: 1;
min-width: 0;
padding: 12px 14px 14px;
border: 0;
background: transparent;
font: 600 13px/1 ui-sans-serif, system-ui;
color: rgba(35, 28, 72, 0.55);
cursor: pointer;
transition: color 0.25s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt21b:hover {
color: rgba(35, 28, 72, 0.85);
}
.tt21b.active {
color: #231c48;
}
.tt21b:focus-visible {
outline: 2px solid #231c48;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.tt21bar {
transition: none;
}
}
```
## JavaScript
```js
/* Ink Slider — toggle .active and slide the ink bar to the active tab.
Re-positions on viewport resize so the bar stays locked to its button. */
(function () {
var nav = document.querySelector(".tt21n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var bar = nav.querySelector(".tt21bar");
var current = null;
function reposition() {
if (!current || !bar) return;
bar.style.left = current.offsetLeft + "px";
bar.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
reposition();
}
btns.forEach(function (b) {
b.addEventListener("click", function () {
activate(b);
});
});
window.addEventListener("resize", reposition);
var initial = nav.querySelector("[data-t].active") || btns[0];
if (initial) activate(initial);
})();
```Here's a working CSS Tab Design from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Ink Slider
Source: https://codefronts.com/navigation/css-tabs/ink-slider/
Solid ink-violet bar slides horizontally beneath the active tab. JS measures offsetLeft + offsetWidth so the bar matches each tab's exact width — no fixed-width assumption — and re-aligns on viewport resize.
## HTML
```html
<div class="tt21">
<nav class="tt21n">
<span class="tt21bar" aria-hidden="true"></span>
<button class="tt21b active" data-t>Overview</button>
<button class="tt21b" data-t>Pricing</button>
<button class="tt21b" data-t>Reviews</button>
<button class="tt21b" data-t>Support</button>
</nav>
</div>
```
## CSS
```css
.tt21 {
background: #faf8f3;
padding: 32px 18px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
min-height: 100vh;
box-sizing: border-box;
width: 100%;
}
.tt21n {
position: relative;
display: flex;
border-bottom: 1px solid rgba(35, 28, 72, 0.12);
min-width: 0;
}
.tt21bar {
position: absolute;
bottom: -1px;
left: 0;
height: 3px;
width: 0;
background: #231c48;
border-radius: 2px 2px 0 0;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
pointer-events: none;
}
.tt21b {
flex: 1;
min-width: 0;
padding: 12px 14px 14px;
border: 0;
background: transparent;
font: 600 13px/1 ui-sans-serif, system-ui;
color: rgba(35, 28, 72, 0.55);
cursor: pointer;
transition: color 0.25s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tt21b:hover {
color: rgba(35, 28, 72, 0.85);
}
.tt21b.active {
color: #231c48;
}
.tt21b:focus-visible {
outline: 2px solid #231c48;
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.tt21bar {
transition: none;
}
}
```
## JavaScript
```js
/* Ink Slider — toggle .active and slide the ink bar to the active tab.
Re-positions on viewport resize so the bar stays locked to its button. */
(function () {
var nav = document.querySelector(".tt21n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var bar = nav.querySelector(".tt21bar");
var current = null;
function reposition() {
if (!current || !bar) return;
bar.style.left = current.offsetLeft + "px";
bar.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
reposition();
}
btns.forEach(function (b) {
b.addEventListener("click", function () {
activate(b);
});
});
window.addEventListener("resize", reposition);
var initial = nav.querySelector("[data-t].active") || btns[0];
if (initial) activate(initial);
})();
```How this works
The .tt21 shell paints a warm off-white canvas at #faf8f3 and hosts a flex .tt21n nav with position: relative, a hairline 1px solid rgba(35, 28, 72, 0.12) baseline, and equal-width .tt21b buttons using flex: 1. The .tt21bar element is absolutely positioned at bottom: -1px with a solid #231c48 ink fill, a 3px height, and a subtle 2px 2px 0 0 top-corner radius that sits over the baseline hairline. Hand-coded vanilla JS reads current.offsetLeft and current.offsetWidth, writes them to inline styles, and a paired cubic-bezier(0.65, 0, 0.35, 1) transition handles the slide.
The palette leans editorial: ink violet #231c48 against paper cream #faf8f3, with resting labels at rgba(35, 28, 72, 0.55) and hover at 0.85 so the active label reads darkest without any weight change. Typography is a copy-paste system stack, ui-sans-serif, system-ui, held at 600 13px/1 so a four-tab row stays honest at narrow widths thanks to white-space: nowrap plus text-overflow: ellipsis.
Semantic HTML uses real button elements, so keyboard tab, Enter, and Space work with zero extra scripting. A :focus-visible ring at 2px solid #231c48 with -2px inset offset satisfies WCAG 2.4.7 without doubling as a click ring. The measured slide runs on transform-adjacent inline styles rather than layout thrash, keeping INP under 200ms on mid-tier Android, and @media (prefers-reduced-motion: reduce) nulls the transition per WCAG 2.3.3. Because the bar is aria-hidden, screen readers announce only the label — pair with role="tablist" when wiring real tabpanels.
Make it yours
- Swap ink
#231c48for your brand token — recolor.tt21barbackground, the.tt21b.activecolor, and the:focus-visibleoutline together so the register stays coherent. - Change the type ramp by editing the
fontshorthand on.tt21b. A500 14px/1Inter or a600 12px/1uppercase treatment withletter-spacing: 0.08emboth suit the ink slider aesthetic. - Shape the bar. Bump
.tt21barheight to4pxand drop the radius to0for a hard broadsheet rule, or raise it to999pxfor a rounded capsule feel. - Dark mode: flip
.tt21background to#0e0b1a, set label color torgba(255, 255, 255, 0.65), active to#ffffff, and the bar to#a78bfaso the ink register keeps its contrast story. - Tailwind arbitrary values:
bg-[#faf8f3]on the wrapper,bg-[#231c48]on the bar,text-[rgb(35_28_72_/_0.55)]on inactive, andtext-[#231c48]on active — the JS positioning logic ports unchanged.
Gotchas — read before shipping
- Resting label color
rgba(35, 28, 72, 0.55)on#faf8f3lands near WCAG 1.4.3 minimums for 13px text. If localisation lengthens labels or you drop weight below 600, lift resting opacity to 0.7 or verify with a contrast checker. - The JS binds click only. For WCAG 2.1.1 you already get keyboard activation from real buttons, but if you migrate to the ARIA tabs pattern add role="tablist", role="tab", aria-selected, and Left/Right arrow-key navigation with tabindex roving.
- React StrictMode double-invokes effects in dev, so binding the resize listener inside useEffect without a cleanup will fire the reposition twice. Return a cleanup that removes the listener and clear current to keep the bar honest across remounts.
- Because the bar reads offsetLeft and offsetWidth synchronously, mounting inside a display:none parent yields zeros and the bar stays hidden. Trigger a reposition on the parent's transitionend or after your tab-panel IntersectionObserver reports visible.
- Next.js App Router streams the shell before hydration; the bar renders at width: 0 until JS runs, so there is no CLS but the active label sits without a rule for a beat. Ship an SSR-safe initial inline style hint if that flash bothers you.
- Safari 15.4 gained :focus-visible, so older iOS may show the outline on tap. If your analytics still see meaningful iOS 15.3 traffic, add outline: none under @supports not selector(:focus-visible) or accept the small cosmetic hit for WCAG 2.4.7 compliance.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 90+ | 15.4+ | 107+ | 90+ |
Uses offsetLeft/offsetWidth measurement and cubic-bezier transitions supported everywhere; :focus-visible needs Safari 15.4.