32 Tailwind CSS Tabs
01 / 32
Pure TailwindMIT licensed

Tailwind CSS Tabs with Sliding Underline Indicator

The most-searched tab pattern: an ink underline that glides beneath the active tab. The CSS original measured offsetLeft in JavaScript; the Tailwind port sets an index with has-[#ttb01-r2:checked]:[--i:1] and moves the bar with translate-x-[calc(var(--i)*100%)]. Zero JavaScript, no resize listener, and the panels fade in with starting:.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="ttb-01 group/tabs grid min-h-svh w-full place-items-center bg-ttb01-bg px-5 py-14 font-ttb01-sans text-ttb01-ink" aria-label="Sliding underline tabs demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="w-full max-w-xl overflow-hidden rounded-2xl bg-ttb01-card shadow-[0_1px_2px_rgb(35_28_72/0.06),0_16px_40px_-16px_rgb(35_28_72/0.22)] ring-1 ring-ttb01-ink/8">
      <div role="radiogroup" aria-label="Product sections" class="relative flex border-b border-ttb01-ink/10 px-2 [--i:0] has-[#ttb01-r2:checked]:[--i:1] has-[#ttb01-r3:checked]:[--i:2] has-[#ttb01-r4:checked]:[--i:3]">
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r1" checked>Overview</label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r2">Pricing</label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r3">Reviews <span class="rounded-full bg-ttb01-ink/8 px-1.5 py-0.5 font-ttb01-mono text-[10.5px] font-medium tabular-nums">128</span></label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r4">Support</label>
        <span aria-hidden="true" class="pointer-events-none absolute -bottom-px left-2 h-[3px] w-[calc((100%-16px)/4)] translate-x-[calc(var(--i)*100%)] rounded-t-full bg-ttb01-ink transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:transition-none"></span>
      </div>
      <div class="px-6 py-5 text-[15px] leading-relaxed">
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r1:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Everything in one workspace</h3><p class="mt-1 text-ttb01-mut text-pretty">Plans, documents and conversations live side by side, so context never gets lost between tools.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r2:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Simple, per-seat pricing</h3><p class="mt-1 text-ttb01-mut text-pretty">One plan for small teams, one for organisations that need SSO and audit logs. Switch any time.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r3:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">What teams are saying</h3><p class="mt-1 text-ttb01-mut text-pretty">Reviews from design, engineering and operations teams who moved their planning here.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r4:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Help when you need it</h3><p class="mt-1 text-ttb01-mut text-pretty">Guides, a searchable help centre and a human reply within one business day.</p></div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full border border-ttb01-ink/10 bg-white/70 px-3.5 py-2 font-ttb01-mono text-[11.5px] text-ttb01-mut">has-[#r2:checked]:[--i:1] · translate-x-[calc(var(--i)*100%)] · 0 KB JS</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-ttb01-bg: #f6f4ee;
  --color-ttb01-bg: oklch(0.967 0.008 91.5);
  --color-ttb01-card: #ffffff;
  --color-ttb01-card: oklch(1.000 0 0);
  --color-ttb01-ink: #231c48;
  --color-ttb01-ink: oklch(0.262 0.079 286.9);
  --color-ttb01-mut: #5c5876;
  --color-ttb01-mut: oklch(0.476 0.048 290.6);
  --font-ttb01-sans: 'Geist', system-ui, sans-serif;
  --font-ttb01-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-01 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-ttb01-bg: #f6f4ee;
  --color-ttb01-bg: oklch(0.967 0.008 91.5);
  --color-ttb01-card: #ffffff;
  --color-ttb01-card: oklch(1.000 0 0);
  --color-ttb01-ink: #231c48;
  --color-ttb01-ink: oklch(0.262 0.079 286.9);
  --color-ttb01-mut: #5c5876;
  --color-ttb01-mut: oklch(0.476 0.048 290.6);
  --font-ttb01-sans: 'Geist', system-ui, sans-serif;
  --font-ttb01-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-01 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The measuring controller (offsetLeft, offsetWidth, a resize listener) was deleted; the bar position is translate-x-[calc(var(--i)*100%)] and the index is set by has-[#…:checked]:[--i:N] on the radiogroup.
  • .tt21b.active became has-checked: on a label that wraps its own radio — the buttons became a native radiogroup with arrow-key navigation for free.
  • UI upgrade from the CSS original: the strip now sits in a card with real panels that fade in (starting:opacity-0), a count badge on Reviews, and 52px tab targets (were ~39px).
  • UI upgrade: resting labels raised from 55% alpha to a solid 6.6:1 muted ink, and there is no unstyled bar flash before hydration — the bar is correct on first paint.
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 demo 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: Tailwind CSS Tabs with Sliding Underline Indicator
Source: https://codefronts.com/navigation/tailwind-css-tabs/tailwind-css-tabs-sliding-underline-indicator/

The most-searched tab pattern: an ink underline that glides beneath the active tab. The CSS original measured offsetLeft in JavaScript; the Tailwind port sets an index with has-[#ttb01-r2:checked]:[--i:1] and moves the bar with translate-x-[calc(var(--i)*100%)]. Zero JavaScript, no resize listener, and the panels fade in with starting:.
## HTML
```html
<section class="ttb-01 group/tabs grid min-h-svh w-full place-items-center bg-ttb01-bg px-5 py-14 font-ttb01-sans text-ttb01-ink" aria-label="Sliding underline tabs demo">
  <div class="grid w-full justify-items-center gap-7">
    <div class="w-full max-w-xl overflow-hidden rounded-2xl bg-ttb01-card shadow-[0_1px_2px_rgb(35_28_72/0.06),0_16px_40px_-16px_rgb(35_28_72/0.22)] ring-1 ring-ttb01-ink/8">
      <div role="radiogroup" aria-label="Product sections" class="relative flex border-b border-ttb01-ink/10 px-2 [--i:0] has-[#ttb01-r2:checked]:[--i:1] has-[#ttb01-r3:checked]:[--i:2] has-[#ttb01-r4:checked]:[--i:3]">
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r1" checked>Overview</label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r2">Pricing</label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r3">Reviews <span class="rounded-full bg-ttb01-ink/8 px-1.5 py-0.5 font-ttb01-mono text-[10.5px] font-medium tabular-nums">128</span></label>
        <label class="flex min-h-13 flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold text-ttb01-mut transition-colors hover:text-ttb01-ink has-checked:text-ttb01-ink has-focus-visible:outline-2 has-focus-visible:-outline-offset-4 has-focus-visible:outline-ttb01-ink"><input class="sr-only" type="radio" name="ttb01" id="ttb01-r4">Support</label>
        <span aria-hidden="true" class="pointer-events-none absolute -bottom-px left-2 h-[3px] w-[calc((100%-16px)/4)] translate-x-[calc(var(--i)*100%)] rounded-t-full bg-ttb01-ink transition-transform duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:transition-none"></span>
      </div>
      <div class="px-6 py-5 text-[15px] leading-relaxed">
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r1:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Everything in one workspace</h3><p class="mt-1 text-ttb01-mut text-pretty">Plans, documents and conversations live side by side, so context never gets lost between tools.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r2:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Simple, per-seat pricing</h3><p class="mt-1 text-ttb01-mut text-pretty">One plan for small teams, one for organisations that need SSO and audit logs. Switch any time.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r3:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">What teams are saying</h3><p class="mt-1 text-ttb01-mut text-pretty">Reviews from design, engineering and operations teams who moved their planning here.</p></div>
        <div class="hidden transition-[opacity,translate] duration-300 group-has-[#ttb01-r4:checked]/tabs:block starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none"><h3 class="font-semibold">Help when you need it</h3><p class="mt-1 text-ttb01-mut text-pretty">Guides, a searchable help centre and a human reply within one business day.</p></div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full border border-ttb01-ink/10 bg-white/70 px-3.5 py-2 font-ttb01-mono text-[11.5px] text-ttb01-mut">has-[#r2:checked]:[--i:1] · translate-x-[calc(var(--i)*100%)] · 0 KB JS</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-ttb01-bg: #f6f4ee;
  --color-ttb01-bg: oklch(0.967 0.008 91.5);
  --color-ttb01-card: #ffffff;
  --color-ttb01-card: oklch(1.000 0 0);
  --color-ttb01-ink: #231c48;
  --color-ttb01-ink: oklch(0.262 0.079 286.9);
  --color-ttb01-mut: #5c5876;
  --color-ttb01-mut: oklch(0.476 0.048 290.6);
  --font-ttb01-sans: 'Geist', system-ui, sans-serif;
  --font-ttb01-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-01 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Measuring was arithmetic all along. The four tabs are flex-1, so every tab is a quarter of the row. The bar is a quarter wide and translates by its own width times the index:

<div role="radiogroup" class="relative flex px-2 [--i:0]
  has-[#ttb01-r2:checked]:[--i:1] has-[#ttb01-r3:checked]:[--i:2] …">
  …
  <span class="absolute left-2 w-[calc((100%-16px)/4)]
    translate-x-[calc(var(--i)*100%)] transition-transform"></span>

Why the radio sits inside the label. peer-checked: compiles to .peer:checked ~ * and would light up every later label in the row. Nesting the input lets each label ask only about itself with has-checked:.

The radios are visually hidden with sr-only, never hidden, so the row is still one Tab stop and the arrow keys move the selection.

Make it yours

  • Five tabs: change the bar to w-[calc((100%-16px)/5)] and add has-[#ttb01-r5:checked]:[--i:4].
  • Pill indicator: swap the bar for inset-y-1.5 rounded-lg bg-ttb01-ink/6 — same index, different shape.
  • Re-theme: edit --color-ttb01-ink; label, bar and focus ring all follow.

Gotchas — read before shipping

  • The index trick needs equal-width tabs (flex-1). For content-sized tabs keep a measuring script, or use CSS anchor positioning where supported.
  • Translate percentages are relative to the bar itself — keep the bar exactly one tab wide or each step overshoots.
  • Give every radio in the group the same name and a unique id; the ids are what the has-[…] variants read.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() drives both the index and the panels (Firefox 121+), inside Tailwind v4's own floor. starting: is progressive.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…