32 CSS Tab Designs13 / 32
Specimen No.
Tab labels read as letterpress specimen slugs. The active tab is announced by a 4-digit serial that flips digits on activation, and a mint underline draws under the active label.
Published Updated
The code
<div class="tt01">
<div class="tt01serial" data-tt01-serial>0317</div>
<nav class="tt01n">
<button class="tt01b active" data-t data-tt01="0317">Helvetica</button>
<button class="tt01b" data-t data-tt01="0211">Bodoni</button>
<button class="tt01b" data-t data-tt01="0489">Futura</button>
<span class="tt01rule"> </span>
</nav>
<div class="tt01p active" data-p>Cut by Max Miedinger, 1957. Neo-grotesque sans.</div>
<div class="tt01p" data-p>Cut by Giambattista Bodoni, 1798. Modern serif.</div>
<div class="tt01p" data-p>Cut by Paul Renner, 1927. Geometric sans.</div>
</div><div class="tt01">
<div class="tt01serial" data-tt01-serial>0317</div>
<nav class="tt01n">
<button class="tt01b active" data-t data-tt01="0317">Helvetica</button>
<button class="tt01b" data-t data-tt01="0211">Bodoni</button>
<button class="tt01b" data-t data-tt01="0489">Futura</button>
<span class="tt01rule"> </span>
</nav>
<div class="tt01p active" data-p>Cut by Max Miedinger, 1957. Neo-grotesque sans.</div>
<div class="tt01p" data-p>Cut by Giambattista Bodoni, 1798. Modern serif.</div>
<div class="tt01p" data-p>Cut by Paul Renner, 1927. Geometric sans.</div>
</div>.tt01 {
min-height: 100vh;
background: #1f2326;
padding: 18px 22px 16px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt01serial {
font: 800 36px/1 ui-monospace, monospace;
color: #51d5c2;
letter-spacing: 0.04em;
margin-bottom: 6px;
transition: opacity 0.15s;
}
.tt01serial.fade {
opacity: 0.2;
}
.tt01n {
position: relative;
display: flex;
gap: 0;
border-top: 1.5px solid rgba(81, 213, 194, 0.25);
border-bottom: 1px solid rgba(81, 213, 194, 0.12);
}
.tt01b {
flex: 1;
padding: 12px 8px;
border: 0;
background: transparent;
font: 700 11px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.45);
cursor: pointer;
border-right: 1px solid rgba(81, 213, 194, 0.1);
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt01b:last-child {
border-right: 0;
}
.tt01b:hover {
color: rgba(232, 226, 208, 0.75);
}
.tt01b.active {
color: #51d5c2;
transform: translateY(-1px);
}
.tt01rule {
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: #51d5c2;
transition: left 0.4s cubic-bezier(0.65, 0, 0.35, 1), width 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt01p {
display: none;
padding-top: 12px;
font: italic 11px/1.6 ui-serif, Georgia;
color: rgba(232, 226, 208, 0.7);
}
.tt01p.active {
display: block;
}.tt01 {
min-height: 100vh;
background: #1f2326;
padding: 18px 22px 16px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt01serial {
font: 800 36px/1 ui-monospace, monospace;
color: #51d5c2;
letter-spacing: 0.04em;
margin-bottom: 6px;
transition: opacity 0.15s;
}
.tt01serial.fade {
opacity: 0.2;
}
.tt01n {
position: relative;
display: flex;
gap: 0;
border-top: 1.5px solid rgba(81, 213, 194, 0.25);
border-bottom: 1px solid rgba(81, 213, 194, 0.12);
}
.tt01b {
flex: 1;
padding: 12px 8px;
border: 0;
background: transparent;
font: 700 11px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.45);
cursor: pointer;
border-right: 1px solid rgba(81, 213, 194, 0.1);
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt01b:last-child {
border-right: 0;
}
.tt01b:hover {
color: rgba(232, 226, 208, 0.75);
}
.tt01b.active {
color: #51d5c2;
transform: translateY(-1px);
}
.tt01rule {
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: #51d5c2;
transition: left 0.4s cubic-bezier(0.65, 0, 0.35, 1), width 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt01p {
display: none;
padding-top: 12px;
font: italic 11px/1.6 ui-serif, Georgia;
color: rgba(232, 226, 208, 0.7);
}
.tt01p.active {
display: block;
}/* Specimen No. — toggle .active and update sliding rule + flip serial.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt01n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var serial = document.querySelector("[data-tt01-serial]");
var rule = document.querySelector(".tt01rule");
var pnls = document.querySelectorAll(".tt01p");
var current = null;
function reposition() {
if (!current || !rule) return;
rule.style.left = current.offsetLeft + "px";
rule.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
var i = Array.prototype.indexOf.call(btns, btn);
pnls.forEach(function (p, j) {
p.classList.toggle("active", j === i);
});
reposition();
if (serial && btn.dataset.tt01) {
serial.classList.add("fade");
setTimeout(function () {
serial.textContent = btn.dataset.tt01;
serial.classList.remove("fade");
}, 150);
}
}
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);
})();/* Specimen No. — toggle .active and update sliding rule + flip serial.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt01n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var serial = document.querySelector("[data-tt01-serial]");
var rule = document.querySelector(".tt01rule");
var pnls = document.querySelectorAll(".tt01p");
var current = null;
function reposition() {
if (!current || !rule) return;
rule.style.left = current.offsetLeft + "px";
rule.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
var i = Array.prototype.indexOf.call(btns, btn);
pnls.forEach(function (p, j) {
p.classList.toggle("active", j === i);
});
reposition();
if (serial && btn.dataset.tt01) {
serial.classList.add("fade");
setTimeout(function () {
serial.textContent = btn.dataset.tt01;
serial.classList.remove("fade");
}, 150);
}
}
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: Specimen No.
Source: https://codefronts.com/navigation/css-tabs/specimen-no/
Tab labels read as letterpress specimen slugs. The active tab is announced by a 4-digit serial that flips digits on activation, and a mint underline draws under the active label.
## HTML
```html
<div class="tt01">
<div class="tt01serial" data-tt01-serial>0317</div>
<nav class="tt01n">
<button class="tt01b active" data-t data-tt01="0317">Helvetica</button>
<button class="tt01b" data-t data-tt01="0211">Bodoni</button>
<button class="tt01b" data-t data-tt01="0489">Futura</button>
<span class="tt01rule"> </span>
</nav>
<div class="tt01p active" data-p>Cut by Max Miedinger, 1957. Neo-grotesque sans.</div>
<div class="tt01p" data-p>Cut by Giambattista Bodoni, 1798. Modern serif.</div>
<div class="tt01p" data-p>Cut by Paul Renner, 1927. Geometric sans.</div>
</div>
```
## CSS
```css
.tt01 {
min-height: 100vh;
background: #1f2326;
padding: 18px 22px 16px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt01serial {
font: 800 36px/1 ui-monospace, monospace;
color: #51d5c2;
letter-spacing: 0.04em;
margin-bottom: 6px;
transition: opacity 0.15s;
}
.tt01serial.fade {
opacity: 0.2;
}
.tt01n {
position: relative;
display: flex;
gap: 0;
border-top: 1.5px solid rgba(81, 213, 194, 0.25);
border-bottom: 1px solid rgba(81, 213, 194, 0.12);
}
.tt01b {
flex: 1;
padding: 12px 8px;
border: 0;
background: transparent;
font: 700 11px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.45);
cursor: pointer;
border-right: 1px solid rgba(81, 213, 194, 0.1);
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt01b:last-child {
border-right: 0;
}
.tt01b:hover {
color: rgba(232, 226, 208, 0.75);
}
.tt01b.active {
color: #51d5c2;
transform: translateY(-1px);
}
.tt01rule {
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: #51d5c2;
transition: left 0.4s cubic-bezier(0.65, 0, 0.35, 1), width 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt01p {
display: none;
padding-top: 12px;
font: italic 11px/1.6 ui-serif, Georgia;
color: rgba(232, 226, 208, 0.7);
}
.tt01p.active {
display: block;
}
```
## JavaScript
```js
/* Specimen No. — toggle .active and update sliding rule + flip serial.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt01n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var serial = document.querySelector("[data-tt01-serial]");
var rule = document.querySelector(".tt01rule");
var pnls = document.querySelectorAll(".tt01p");
var current = null;
function reposition() {
if (!current || !rule) return;
rule.style.left = current.offsetLeft + "px";
rule.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
var i = Array.prototype.indexOf.call(btns, btn);
pnls.forEach(function (p, j) {
p.classList.toggle("active", j === i);
});
reposition();
if (serial && btn.dataset.tt01) {
serial.classList.add("fade");
setTimeout(function () {
serial.textContent = btn.dataset.tt01;
serial.classList.remove("fade");
}, 150);
}
}
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: Specimen No.
Source: https://codefronts.com/navigation/css-tabs/specimen-no/
Tab labels read as letterpress specimen slugs. The active tab is announced by a 4-digit serial that flips digits on activation, and a mint underline draws under the active label.
## HTML
```html
<div class="tt01">
<div class="tt01serial" data-tt01-serial>0317</div>
<nav class="tt01n">
<button class="tt01b active" data-t data-tt01="0317">Helvetica</button>
<button class="tt01b" data-t data-tt01="0211">Bodoni</button>
<button class="tt01b" data-t data-tt01="0489">Futura</button>
<span class="tt01rule"> </span>
</nav>
<div class="tt01p active" data-p>Cut by Max Miedinger, 1957. Neo-grotesque sans.</div>
<div class="tt01p" data-p>Cut by Giambattista Bodoni, 1798. Modern serif.</div>
<div class="tt01p" data-p>Cut by Paul Renner, 1927. Geometric sans.</div>
</div>
```
## CSS
```css
.tt01 {
min-height: 100vh;
background: #1f2326;
padding: 18px 22px 16px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt01serial {
font: 800 36px/1 ui-monospace, monospace;
color: #51d5c2;
letter-spacing: 0.04em;
margin-bottom: 6px;
transition: opacity 0.15s;
}
.tt01serial.fade {
opacity: 0.2;
}
.tt01n {
position: relative;
display: flex;
gap: 0;
border-top: 1.5px solid rgba(81, 213, 194, 0.25);
border-bottom: 1px solid rgba(81, 213, 194, 0.12);
}
.tt01b {
flex: 1;
padding: 12px 8px;
border: 0;
background: transparent;
font: 700 11px/1 ui-monospace, monospace;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.45);
cursor: pointer;
border-right: 1px solid rgba(81, 213, 194, 0.1);
transition: color 0.25s, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.tt01b:last-child {
border-right: 0;
}
.tt01b:hover {
color: rgba(232, 226, 208, 0.75);
}
.tt01b.active {
color: #51d5c2;
transform: translateY(-1px);
}
.tt01rule {
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: #51d5c2;
transition: left 0.4s cubic-bezier(0.65, 0, 0.35, 1), width 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt01p {
display: none;
padding-top: 12px;
font: italic 11px/1.6 ui-serif, Georgia;
color: rgba(232, 226, 208, 0.7);
}
.tt01p.active {
display: block;
}
```
## JavaScript
```js
/* Specimen No. — toggle .active and update sliding rule + flip serial.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt01n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var serial = document.querySelector("[data-tt01-serial]");
var rule = document.querySelector(".tt01rule");
var pnls = document.querySelectorAll(".tt01p");
var current = null;
function reposition() {
if (!current || !rule) return;
rule.style.left = current.offsetLeft + "px";
rule.style.width = current.offsetWidth + "px";
}
function activate(btn) {
current = btn;
btns.forEach(function (b) {
b.classList.toggle("active", b === btn);
});
var i = Array.prototype.indexOf.call(btns, btn);
pnls.forEach(function (p, j) {
p.classList.toggle("active", j === i);
});
reposition();
if (serial && btn.dataset.tt01) {
serial.classList.add("fade");
setTimeout(function () {
serial.textContent = btn.dataset.tt01;
serial.classList.remove("fade");
}, 150);
}
}
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 Specimen No. tabs open with a 36px .tt01serial readout in mint #51d5c2, then a nav of three uppercase labels — Helvetica, Bodoni, Futura — each carrying a data-tt01 attribute holding the specimen slug (0317, 0211, 0489). The nav sits between a 1.5px top border and a 1px bottom border, both in translucent mint. Panels below use display:none by default and display:block when .active is present, rendering the specimen provenance in italic ui-serif.
The script is a scoped IIFE. On click, activate(btn) toggles .active across all buttons, mirrors that state onto the matching panel via Array.prototype.indexOf, then calls reposition to move the 2px .tt01rule underline by setting its left and width from the active button's offsetLeft/offsetWidth. Both transitions run 400ms with cubic-bezier(0.65,0,0.35,1). The active label also lifts with translateY(-1px) using a spring cubic-bezier(0.34,1.56,0.64,1) that overshoots slightly, evoking a letterpress plate tapping down.
The serial flip is the signature move. When a new tab activates, .tt01serial gets a .fade class that drops opacity to 0.2 over 150ms; a setTimeout then swaps textContent to the button's data-tt01 value and removes the fade class, so the digits appear to swap while dimmed. Colour is a two-token system: mint #51d5c2 for active and specimen number, and warm off-white rgba(232,226,208,0.45) for inactive labels on #1f2326 charcoal. The ui-monospace stack at 700 weight with 0.18em tracking reads as a type-catalogue title plate. A resize listener reruns reposition so the underline stays anchored on reflow.
Make it yours
- Replace specimen slugs by editing data-tt01 values on each button and the seed 0317 inside .tt01serial. Keep them 4-digit so the fixed-width mono font does not reflow the header row between activations.
- Retune the flip by adjusting the 150ms setTimeout and the matching opacity 0.15s transition on .tt01serial. Longer fades (250ms) feel editorial; shorter (80ms) reads as an LED counter. Keep both values in sync.
- Swap mint for another spec-plate accent by replacing #51d5c2 across the serial, active label, underline, and both border colours. Sage #8ecfa0 and salmon #f4a988 both hold contrast above 4.5:1 on the #1f2326 ground.
- Change the serif provenance line by editing the italic 11px/1.6 ui-serif, Georgia stack on .tt01p. A Fraunces or Playfair Display variable font emphasises the catalogue register; keep italic for the specimen-note voice.
- Extend to more type families by duplicating a button, wiring a data-tt01 serial, and adding a matching .tt01p panel. The indexOf-based panel toggle handles any count, and flex:1 auto-distributes the tab row width.
Gotchas — read before shipping
- The nav uses raw button elements with no role="tab", role="tablist", or aria-selected attributes. Screen readers hear four buttons, not tabs. Add the ARIA tabs pattern manually if the component ships in a product; the specimen aesthetic is otherwise unusable for AT.
- The .tt01serial flip is a textContent swap on a live region without aria-live. Screen readers may or may not announce the change depending on browser. Add aria-live="polite" and aria-atomic="true" so the specimen number is announced when tabs change.
- The 150ms fade timeout is racy. If a user clicks tabs faster than 150ms, the pending setTimeout from the previous click overwrites the current specimen number. Track the timeout ID and clearTimeout before scheduling a new fade.
- The resize listener attaches to window but never detaches. In React StrictMode this fires twice on mount and never cleans up on unmount, so the underline logic runs on stale button references. Wrap in a lifecycle hook.
- The rgba(232,226,208,0.45) inactive-label colour on #1f2326 is roughly 3.1:1 contrast, under WCAG 2.1 AA for normal text. Bump to 0.65 alpha or lighten the ground to #26292d if the component is a primary nav rather than a decorative specimen frame.
- The querySelector calls target class names globally (.tt01n, [data-tt01-serial]), which breaks if two Specimen No. blocks render on the same page. Scope the script by passing the root element in, or rename per instance.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 60+ | 12+ | 60+ | 60+ |
Uses baseline flexbox, transitions, and dataset APIs. The spring cubic-bezier and 400ms slide transitions are widely supported. No prefers-reduced-motion block ships — add one that cancels the underline transition and the serial fade for INP-safe delivery.