32 CSS Tab Designs18 / 32
Underset Caps
Tiny uppercase mono labels above a single thick rule. The rule slides under the active label — measured live, like a sliding bookmark on a typecase shelf.
Published Updated
The code
<div class="tt06">
<nav class="tt06n">
<button class="tt06b active" data-t>Volume I</button>
<button class="tt06b" data-t>Volume II</button>
<button class="tt06b" data-t>Volume III</button>
<button class="tt06b" data-t>Volume IV</button>
<span class="tt06rule"> </span>
</nav>
<div class="tt06p active" data-p>The Origins, 1450–1600.</div>
<div class="tt06p" data-p>The Reformation, 1600–1750.</div>
<div class="tt06p" data-p>The Industrial Cut, 1750–1900.</div>
<div class="tt06p" data-p>The Digital Era, 1980 onward.</div>
</div><div class="tt06">
<nav class="tt06n">
<button class="tt06b active" data-t>Volume I</button>
<button class="tt06b" data-t>Volume II</button>
<button class="tt06b" data-t>Volume III</button>
<button class="tt06b" data-t>Volume IV</button>
<span class="tt06rule"> </span>
</nav>
<div class="tt06p active" data-p>The Origins, 1450–1600.</div>
<div class="tt06p" data-p>The Reformation, 1600–1750.</div>
<div class="tt06p" data-p>The Industrial Cut, 1750–1900.</div>
<div class="tt06p" data-p>The Digital Era, 1980 onward.</div>
</div>.tt06 {
min-height: 100vh;
background: #1a1a1a;
padding: 28px 22px 22px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt06n {
position: relative;
display: flex;
gap: 24px;
padding-bottom: 14px;
}
.tt06b {
border: 0;
background: transparent;
padding: 0;
font: 700 9px/1 ui-monospace, monospace;
letter-spacing: 0.28em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.5);
cursor: pointer;
transition: color 0.25s;
}
.tt06b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt06b.active {
color: #e8e2d0;
}
.tt06rule {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 6px;
background: #c43a32;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt06p {
display: none;
padding-top: 18px;
font: italic 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
}
.tt06p.active {
display: block;
}.tt06 {
min-height: 100vh;
background: #1a1a1a;
padding: 28px 22px 22px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt06n {
position: relative;
display: flex;
gap: 24px;
padding-bottom: 14px;
}
.tt06b {
border: 0;
background: transparent;
padding: 0;
font: 700 9px/1 ui-monospace, monospace;
letter-spacing: 0.28em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.5);
cursor: pointer;
transition: color 0.25s;
}
.tt06b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt06b.active {
color: #e8e2d0;
}
.tt06rule {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 6px;
background: #c43a32;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt06p {
display: none;
padding-top: 18px;
font: italic 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
}
.tt06p.active {
display: block;
}/* Underset Caps — toggle .active and slide horizontal rule under active.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt06n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var rule = nav.querySelector(".tt06rule");
var pnls = document.querySelectorAll(".tt06p");
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();
}
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);
})();/* Underset Caps — toggle .active and slide horizontal rule under active.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt06n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var rule = nav.querySelector(".tt06rule");
var pnls = document.querySelectorAll(".tt06p");
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();
}
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: Underset Caps
Source: https://codefronts.com/navigation/css-tabs/underset-caps/
Tiny uppercase mono labels above a single thick rule. The rule slides under the active label — measured live, like a sliding bookmark on a typecase shelf.
## HTML
```html
<div class="tt06">
<nav class="tt06n">
<button class="tt06b active" data-t>Volume I</button>
<button class="tt06b" data-t>Volume II</button>
<button class="tt06b" data-t>Volume III</button>
<button class="tt06b" data-t>Volume IV</button>
<span class="tt06rule"> </span>
</nav>
<div class="tt06p active" data-p>The Origins, 1450–1600.</div>
<div class="tt06p" data-p>The Reformation, 1600–1750.</div>
<div class="tt06p" data-p>The Industrial Cut, 1750–1900.</div>
<div class="tt06p" data-p>The Digital Era, 1980 onward.</div>
</div>
```
## CSS
```css
.tt06 {
min-height: 100vh;
background: #1a1a1a;
padding: 28px 22px 22px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt06n {
position: relative;
display: flex;
gap: 24px;
padding-bottom: 14px;
}
.tt06b {
border: 0;
background: transparent;
padding: 0;
font: 700 9px/1 ui-monospace, monospace;
letter-spacing: 0.28em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.5);
cursor: pointer;
transition: color 0.25s;
}
.tt06b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt06b.active {
color: #e8e2d0;
}
.tt06rule {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 6px;
background: #c43a32;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt06p {
display: none;
padding-top: 18px;
font: italic 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
}
.tt06p.active {
display: block;
}
```
## JavaScript
```js
/* Underset Caps — toggle .active and slide horizontal rule under active.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt06n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var rule = nav.querySelector(".tt06rule");
var pnls = document.querySelectorAll(".tt06p");
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();
}
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: Underset Caps
Source: https://codefronts.com/navigation/css-tabs/underset-caps/
Tiny uppercase mono labels above a single thick rule. The rule slides under the active label — measured live, like a sliding bookmark on a typecase shelf.
## HTML
```html
<div class="tt06">
<nav class="tt06n">
<button class="tt06b active" data-t>Volume I</button>
<button class="tt06b" data-t>Volume II</button>
<button class="tt06b" data-t>Volume III</button>
<button class="tt06b" data-t>Volume IV</button>
<span class="tt06rule"> </span>
</nav>
<div class="tt06p active" data-p>The Origins, 1450–1600.</div>
<div class="tt06p" data-p>The Reformation, 1600–1750.</div>
<div class="tt06p" data-p>The Industrial Cut, 1750–1900.</div>
<div class="tt06p" data-p>The Digital Era, 1980 onward.</div>
</div>
```
## CSS
```css
.tt06 {
min-height: 100vh;
background: #1a1a1a;
padding: 28px 22px 22px;
font-family: ui-monospace, monospace;
width: 100%;
}
.tt06n {
position: relative;
display: flex;
gap: 24px;
padding-bottom: 14px;
}
.tt06b {
border: 0;
background: transparent;
padding: 0;
font: 700 9px/1 ui-monospace, monospace;
letter-spacing: 0.28em;
text-transform: uppercase;
color: rgba(232, 226, 208, 0.5);
cursor: pointer;
transition: color 0.25s;
}
.tt06b:hover {
color: rgba(232, 226, 208, 0.85);
}
.tt06b.active {
color: #e8e2d0;
}
.tt06rule {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 6px;
background: #c43a32;
transition: left 0.45s cubic-bezier(0.65, 0, 0.35, 1), width 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.tt06p {
display: none;
padding-top: 18px;
font: italic 12px/1.6 ui-serif, Georgia, serif;
color: rgba(232, 226, 208, 0.7);
}
.tt06p.active {
display: block;
}
```
## JavaScript
```js
/* Underset Caps — toggle .active and slide horizontal rule under active.
Re-positions the sliding rule on viewport resize. */
(function () {
var nav = document.querySelector(".tt06n");
if (!nav) return;
var btns = nav.querySelectorAll("[data-t]");
var rule = nav.querySelector(".tt06rule");
var pnls = document.querySelectorAll(".tt06p");
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();
}
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
Underset Caps is a JavaScript-assisted tab pattern styled as a pressroom shelf. Labels (.tt06b) are ui-monospace at 700 9px/1 with a heavy letter-spacing: 0.28em, uppercased so they read as small-caps chapter markers. The stage sits on a matte #1a1a1a ground with bone label color rgba(232, 226, 208, 0.5) for inactive and full opacity #e8e2d0 for the active state, echoing an editorial contents page.
The rule is a single absolutely-positioned .tt06rule span, 6px tall in pressroom red #c43a32, pinned to bottom: 0 of .tt06n. Its left and width are written from JavaScript on every activation: rule.style.left = current.offsetLeft + "px" and rule.style.width = current.offsetWidth + "px". Both properties animate over 0.45s with cubic-bezier(0.65, 0, 0.35, 1) — a firm ease-in-out that lands the rule with a slight resolve, like a bookmark sliding into a typecase.
The JS is an IIFE, dependency-free, and roughly forty lines. It grabs [data-t] buttons, wires a click listener that toggles .active on the button and on the matching [data-p] panel, then calls reposition(). A window resize handler re-runs reposition so the rule stays locked when viewport width changes. The panels use ui-serif italic at 12px/1.6, again mimicking a captioned image plate. The buttons are semantic button elements, so keyboard tab order and screen-reader announcement work without role="tab" plumbing. The whole component is copy-paste, framework-agnostic, scoped under .tt06, and SSR-safe — the JS only runs once .tt06n exists in the DOM.
Make it yours
- Retint the rule from pressroom red #c43a32 to a foundry accent — a marigold #f5cb1a reads industrial; a paper cream #e8e2d0 matches the label color for a subtler shelf-line. Only .tt06rule background needs the change.
- Adjust the tracking. The current letter-spacing: 0.28em reads as period small-caps; drop to 0.16em for a functional sublabel look, or push to 0.36em for museum-plaque register. Also raise font-size to 10.5px if you widen tracking.
- Change the rule height from 6px to 2px for a thin measured line, or 10px for a full press bar. Pair a thick rule with darker labels and lighter active states, since the rule then reads as the primary hierarchy signal.
- Swap monospace for a serif specimen face like Fraunces or Recoleta at 700 10px/1. Keep the uppercase transform and the wide tracking — the combination of serif + wide caps reads high-editorial rather than typewritten.
- Add a 0.15s label color transition on .tt06b for a smoother activation. The source only transitions color on hover; matching the timing on the checked selector gives a synchronized handoff between label brightening and rule slide.
Gotchas — read before shipping
- The rule position is computed from offsetLeft/offsetWidth. Both read 0 when the element is display: none. If you mount .tt06 inside a hidden accordion, call reposition() after the accordion opens or the rule will collapse to the left edge.
- The IIFE runs at parse time and grabs the first .tt06n only. If you render two Underset Caps demos on one page, only the first will activate — refactor the IIFE into a per-nav loop keyed on a data attribute before shipping duplicates.
- The click handler does not fire on keyboard Enter/Space automatically on all browsers if you swap button for a div. Keep the semantic button element or wire an explicit keydown listener; native buttons dispatch click on both keys for free.
- The rule sits at bottom: 0 of .tt06n, which has padding-bottom: 14px. If you tighten that padding to zero, the rule will clip against the panels below — either keep the padding or move the rule to a positive bottom offset.
- The resize listener has no debounce. On resize storms (window drag on macOS) it will thrash layout reads. For long-lived apps, wrap reposition in a requestAnimationFrame guard or a small setTimeout debounce; it is INP-safe on demo pages but not necessarily on production dashboards.
- There is no prefers-reduced-motion guard on .tt06rule. Users with reduced motion still see the slide — add a media query that sets transition: none on the rule if you ship this in accessibility-strict environments.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 88+ | 14+ | 78+ | 88+ |
The JS uses only querySelectorAll, offsetLeft, and classList — universally supported. No :has(), no variable fonts, no modern CSS features. Works in every currently-shipping browser.