32 CSS Tab Designs18 / 32

CSS + JSMIT licensed

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

Live Demo
Try it

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>
.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);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…