32 CSS Tab Designs29 / 32

CSS + JSMIT licensed

Sliding Pill

Icon + label tabs on cream stock. A solid indigo pill is the indicator; it physically slides between tabs on click, with the active label inverting to cream.

Published Updated

Live Demo
Try it

The code

<div class="tt17">
  <nav class="tt17n">
    <span class="tt17pill" aria-hidden="true"> </span>
    <button class="tt17b active" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <rect x="3" y="3" width="7" height="7" />
        <rect x="14" y="3" width="7" height="7" />
        <rect x="3" y="14" width="7" height="7" />
        <rect x="14" y="14" width="7" height="7" />
      </svg>
      <span class="tt17l">Grid</span>
    </button>
    <button class="tt17b" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <line x1="8" y1="6" x2="21" y2="6" />
        <line x1="8" y1="12" x2="21" y2="12" />
        <line x1="8" y1="18" x2="21" y2="18" />
        <circle cx="4" cy="6" r="1" />
        <circle cx="4" cy="12" r="1" />
        <circle cx="4" cy="18" r="1" />
      </svg>
      <span class="tt17l">List</span>
    </button>
    <button class="tt17b" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <rect x="3" y="4" width="18" height="6" rx="1" />
        <rect x="3" y="14" width="18" height="6" rx="1" />
      </svg>
      <span class="tt17l">Cards</span>
    </button>
  </nav>
</div>
.tt17 {
  background: #f4f3ee;
  padding: 28px 22px;
  font-family: ui-sans-serif, system-ui, sans-serif;
  min-height: 100vh;
  display: flex;
  align-items: center;
  width: 100%;
}

.tt17n {
  position: relative;
  display: flex;
  gap: 0;
  padding: 6px;
  background: rgba(59, 58, 140, 0.06);
  border-radius: 999px;
  flex: 1;
}

.tt17pill {
  position: absolute;
  top: 6px;
  left: 6px;
  height: calc(100% - 12px);
  width: 0;
  background: #3b3a8c;
  border-radius: 999px;
  transition: left 0.5s cubic-bezier(0.65, 0, 0.35, 1), width 0.5s cubic-bezier(0.65, 0, 0.35, 1);
  z-index: 0;
}

.tt17b {
  position: relative;
  z-index: 1;
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 14px;
  border: 0;
  background: transparent;
  font: 600 13px/1 ui-sans-serif, system-ui;
  letter-spacing: 0.02em;
  color: rgba(59, 58, 140, 0.65);
  cursor: pointer;
  transition: color 0.3s 0.05s;
}

.tt17b:hover {
  color: rgba(59, 58, 140, 0.95);
}

.tt17b.active {
  color: #f4f3ee;
}

.tt17i {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Sliding Pill — toggle .active and slide solid pill between tabs.
   Re-positions the pill on viewport resize. */
(function () {
  var nav = document.querySelector(".tt17n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var pill = nav.querySelector(".tt17pill");
  var current = null;

  function reposition() {
    if (!current || !pill) return;
    pill.style.left = current.offsetLeft + "px";
    pill.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);
})();
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: Sliding Pill
Source: https://codefronts.com/navigation/css-tabs/sliding-pill/

Icon + label tabs on cream stock. A solid indigo pill is the indicator; it physically slides between tabs on click, with the active label inverting to cream.
## HTML
```html
<div class="tt17">
  <nav class="tt17n">
    <span class="tt17pill" aria-hidden="true"> </span>
    <button class="tt17b active" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <rect x="3" y="3" width="7" height="7" />
        <rect x="14" y="3" width="7" height="7" />
        <rect x="3" y="14" width="7" height="7" />
        <rect x="14" y="14" width="7" height="7" />
      </svg>
      <span class="tt17l">Grid</span>
    </button>
    <button class="tt17b" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <line x1="8" y1="6" x2="21" y2="6" />
        <line x1="8" y1="12" x2="21" y2="12" />
        <line x1="8" y1="18" x2="21" y2="18" />
        <circle cx="4" cy="6" r="1" />
        <circle cx="4" cy="12" r="1" />
        <circle cx="4" cy="18" r="1" />
      </svg>
      <span class="tt17l">List</span>
    </button>
    <button class="tt17b" data-t>
      <svg class="tt17i" viewBox="0 0 24 24" aria-hidden="true">
        <rect x="3" y="4" width="18" height="6" rx="1" />
        <rect x="3" y="14" width="18" height="6" rx="1" />
      </svg>
      <span class="tt17l">Cards</span>
    </button>
  </nav>
</div>
```
## CSS
```css
.tt17 {
  background: #f4f3ee;
  padding: 28px 22px;
  font-family: ui-sans-serif, system-ui, sans-serif;
  min-height: 100vh;
  display: flex;
  align-items: center;
  width: 100%;
}

.tt17n {
  position: relative;
  display: flex;
  gap: 0;
  padding: 6px;
  background: rgba(59, 58, 140, 0.06);
  border-radius: 999px;
  flex: 1;
}

.tt17pill {
  position: absolute;
  top: 6px;
  left: 6px;
  height: calc(100% - 12px);
  width: 0;
  background: #3b3a8c;
  border-radius: 999px;
  transition: left 0.5s cubic-bezier(0.65, 0, 0.35, 1), width 0.5s cubic-bezier(0.65, 0, 0.35, 1);
  z-index: 0;
}

.tt17b {
  position: relative;
  z-index: 1;
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 14px;
  border: 0;
  background: transparent;
  font: 600 13px/1 ui-sans-serif, system-ui;
  letter-spacing: 0.02em;
  color: rgba(59, 58, 140, 0.65);
  cursor: pointer;
  transition: color 0.3s 0.05s;
}

.tt17b:hover {
  color: rgba(59, 58, 140, 0.95);
}

.tt17b.active {
  color: #f4f3ee;
}

.tt17i {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
```

## JavaScript
```js
/* Sliding Pill — toggle .active and slide solid pill between tabs.
   Re-positions the pill on viewport resize. */
(function () {
  var nav = document.querySelector(".tt17n");
  if (!nav) return;
  var btns = nav.querySelectorAll("[data-t]");
  var pill = nav.querySelector(".tt17pill");
  var current = null;

  function reposition() {
    if (!current || !pill) return;
    pill.style.left = current.offsetLeft + "px";
    pill.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 .tt17 bar is the refined UI-kit register — icon plus label tabs sitting on cream stock #f4f3ee, wrapped in a 999px-radius pill track tinted rgba(59, 58, 140, 0.06). The indicator is a single absolutely-positioned .tt17pill span, painted solid indigo #3b3a8c, that physically translates between tab positions. Each .tt17b button sits at z-index:1 above the pill, so the active label inverts to cream while inactive labels read at rgba(59, 58, 140, 0.65) and hover-lift to 95%.

A small vanilla-JS module wires the mechanic. On click, the handler toggles the .active class across all [data-t] buttons, then reads offsetLeft and offsetWidth of the current button and writes them to the pill's inline left and width properties. Both animate on a cubic-bezier(0.65, 0, 0.35, 1) curve over 500ms, so the pill glides rather than snapping — the Linear / Vercel / Framer signature. A window resize listener re-runs reposition() so the pill stays locked to the active tab when the viewport reflows or split-view resizes.

The label color transition intentionally lags by 50ms (transition: color 0.3s 0.05s) so the pill arrives at the destination just before the label inverts, selling the illusion that the pill catches the text. The stroke icons ride currentColor, so they invert alongside the label without a second rule. The whole component is framework-agnostic — drop the IIFE into any React, Vue, Svelte, or plain-HTML page and it hydrates on DOM ready without any lifecycle wiring. SSR-safe because the initial .active class is authored in the markup; the JS just enhances subsequent clicks. INP-safe because each click writes two style properties and toggles one class — no layout thrash beyond the natural transition, and the pill track collapses gracefully if JS never loads.

Make it yours

  • Change the indigo pill to any solid brand color — teal #0d9488, graphite #111827, magenta #c026d3. Keep the track tint at 6% opacity of the same hue and the inactive label at 65% opacity so the whole component color-shifts as one unit.
  • Retune the ease: cubic-bezier(0.65, 0, 0.35, 1) is the classic in-out. Try cubic-bezier(0.16, 1, 0.3, 1) for a softer overshoot-free settle, or shorten the 0.5s duration to 0.35s if your tab count grows past four and travel distance shrinks.
  • The 50ms delay on the label color (transition: color 0.3s 0.05s) is what sells the pill catches the label illusion. Bump it to 0.15s for slower travel, or drop to 0 if you want the label to invert instantly on click for higher perceived responsiveness.
  • Swap the stroke icons via inline SVG — the currentColor stroke means any icon you drop inherits the label color transition automatically. Keep stroke-width:1.8 and the 16px viewBox size for optical parity with the Grid / List / Cards trio.
  • For dark mode, invert the stock to #111114, the track to rgba(255,255,255,0.06), and paint the pill in a lighter indigo #818cf8 so it remains the brightest element in the composition — the pill is always the figure, the track is always the ground.

Gotchas — read before shipping

  • The pill reads offsetLeft and offsetWidth from the button. If you wrap the buttons in a container with transform or position:relative that changes the offset parent, the math breaks and the pill jumps to the wrong coordinate. Keep .tt17n as the sole positioned ancestor.
  • The resize listener re-runs synchronously on every viewport tick. On mobile Safari's rubber-band scroll or on iPad Split View drag, this can fire dozens of times per second. Wrap the handler in requestAnimationFrame or a 100ms debounce to keep INP under the 200ms tier-1 threshold.
  • The initial .active class must be authored in the HTML, not applied in a DOMContentLoaded handler. If you defer the class, the pill has zero width on first paint and users see a naked track for a frame — an SSR-safe hydration flash that tier-1 dev-tooling users will screenshot and complain about.
  • The @media (prefers-reduced-motion:reduce) guard isn't in the shipped CSS — add it yourself. Reduce the pill's transition to none for users on that setting; the click still lands, but the 500ms glide becomes an instant jump, satisfying WCAG 2.1 SC 2.3.3.
  • The buttons use flex:1 so all three share the track equally. If your labels vary wildly in length (Grid vs. Notifications vs. Settings), flex:1 gives them equal width but the icon-plus-label group centers inside — check the padding budget stays generous or the longer label clips.
  • The pill sits at z-index:0, buttons at z-index:1. If you drop a decorative shadow or SVG behind the pill without setting its z-index explicitly, browsers stack it above the pill and the indicator disappears — always give sibling decorations an explicit z-index:-1 or place them outside .tt17n.

Browser support

ChromeSafariFirefoxEdge
60+12+60+60+

Uses standard CSS transitions and vanilla-JS offset measurement. Works everywhere the flexbox pill track renders — effectively all evergreen tier-1 traffic since 2018. No modern selectors required.

Techniques used in this demo

Search CodeFronts

Loading…