32 Tailwind CSS Tabs
08 / 32
Pure TailwindMIT licensed

Vertical Tabs with Timeline Progress in Tailwind CSS

A settings sidebar on a timeline rail: hollow dots for each section, a solid glowing dot for the active one, and — new in the port — a progress fill that runs down the rail to the active step. Beside it, the content pane switches; on phones the two stack.

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-08 group/tabs grid min-h-svh w-full place-items-center bg-ttb08-bg px-5 py-14 font-ttb08-sans text-ttb08-ink" aria-label="Vertical timeline tabs demo">
  <div class="grid w-full max-w-2xl justify-items-center gap-7">
    <div class="grid w-full gap-4 rounded-2xl bg-ttb08-card p-4 ring-1 ring-white/8 sm:grid-cols-[210px_1fr]">
      <div role="radiogroup" aria-label="Settings sections" class="relative flex flex-col [--i:0] has-[#ttb08-r2:checked]:[--i:1] has-[#ttb08-r3:checked]:[--i:2] has-[#ttb08-r4:checked]:[--i:3]">
        <span aria-hidden="true" class="absolute top-6 bottom-6 left-[15px] w-0.5 rounded-full bg-ttb08-acc/20"></span>
        <span aria-hidden="true" class="absolute top-6 left-[15px] h-[calc((100%-48px)*var(--i)/3)] w-0.5 rounded-full bg-ttb08-acc transition-[height] duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:transition-none"></span>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r1" checked><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Profile</span><span class="block text-[11px] text-ttb08-dim">Name, photo, bio</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r2"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Account</span><span class="block text-[11px] text-ttb08-dim">Email, password, 2FA</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r3"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Billing</span><span class="block text-[11px] text-ttb08-dim">Plan, invoices</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r4"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Logs</span><span class="block text-[11px] text-ttb08-dim">Sessions, activity</span></span></label>
      </div>
      <div class="rounded-xl bg-white/[0.03] p-5 ring-1 ring-white/6">
        <div class="hidden group-has-[#ttb08-r1:checked]/tabs:block"><h3 class="text-lg font-bold">Profile</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">How you appear to teammates: display name, avatar and a short bio.</p></div>
        <div class="hidden group-has-[#ttb08-r2:checked]/tabs:block"><h3 class="text-lg font-bold">Account</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Sign-in email, password and two-factor authentication.</p></div>
        <div class="hidden group-has-[#ttb08-r3:checked]/tabs:block"><h3 class="text-lg font-bold">Billing</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Your current plan, payment method and downloadable invoices.</p></div>
        <div class="hidden group-has-[#ttb08-r4:checked]/tabs:block"><h3 class="text-lg font-bold">Logs</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Active sessions and a 90-day record of account activity.</p></div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full border border-white/10 px-3.5 py-2 font-ttb08-mono text-[11.5px] text-ttb08-mut">rail fill h-[calc((100%-48px)*var(--i)/3)] · group-has-checked/tab: dot</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-ttb08-bg: #0b0f15;
  --color-ttb08-bg: oklch(0.167 0.014 258.4);
  --color-ttb08-card: #111821;
  --color-ttb08-card: oklch(0.206 0.021 254.9);
  --color-ttb08-ink: #eef4fa;
  --color-ttb08-ink: oklch(0.964 0.010 247.9);
  --color-ttb08-mut: #a7b3c2;
  --color-ttb08-mut: oklch(0.762 0.025 254.0);
  --color-ttb08-dim: #8794a5;
  --color-ttb08-dim: oklch(0.662 0.029 255.1);
  --color-ttb08-acc: #38bdf8;
  --color-ttb08-acc: oklch(0.754 0.139 232.7);
  --color-ttb08-acc-ink: #7dd3fc;
  --color-ttb08-acc-ink: oklch(0.828 0.101 230.3);
  --font-ttb08-sans: 'Geist', system-ui, sans-serif;
  --font-ttb08-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-08 {
  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-ttb08-bg: #0b0f15;
  --color-ttb08-bg: oklch(0.167 0.014 258.4);
  --color-ttb08-card: #111821;
  --color-ttb08-card: oklch(0.206 0.021 254.9);
  --color-ttb08-ink: #eef4fa;
  --color-ttb08-ink: oklch(0.964 0.010 247.9);
  --color-ttb08-mut: #a7b3c2;
  --color-ttb08-mut: oklch(0.762 0.025 254.0);
  --color-ttb08-dim: #8794a5;
  --color-ttb08-dim: oklch(0.662 0.029 255.1);
  --color-ttb08-acc: #38bdf8;
  --color-ttb08-acc: oklch(0.754 0.139 232.7);
  --color-ttb08-acc-ink: #7dd3fc;
  --color-ttb08-acc-ink: oklch(0.828 0.101 230.3);
  --font-ttb08-sans: 'Geist', system-ui, sans-serif;
  --font-ttb08-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-08 {
  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 ::before rail and the dot's margin-left:-29px arithmetic were replaced by two absolutely placed spans under a dot at a known x — no negative-margin geometry to recompute.
  • input:checked + .tt28b .tt28dot became group-has-checked/tab: on the dot.
  • UI upgrade from the CSS original: a progress fill runs down the rail to the active step (h-[calc((100%-48px)*var(--i)/3)]), each step gained a sub-label, and a content pane sits beside the nav.
  • UI upgrade: rows are 48px tall (were ~24px), inactive labels moved from 55% white to a 7.6:1 token, and the layout stacks on phones.
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: Vertical Tabs with Timeline Progress in Tailwind CSS
Source: https://codefronts.com/navigation/tailwind-css-tabs/vertical-tabs-timeline-progress-tailwind-css/

A settings sidebar on a timeline rail: hollow dots for each section, a solid glowing dot for the active one, and — new in the port — a progress fill that runs down the rail to the active step. Beside it, the content pane switches; on phones the two stack.
## HTML
```html
<section class="ttb-08 group/tabs grid min-h-svh w-full place-items-center bg-ttb08-bg px-5 py-14 font-ttb08-sans text-ttb08-ink" aria-label="Vertical timeline tabs demo">
  <div class="grid w-full max-w-2xl justify-items-center gap-7">
    <div class="grid w-full gap-4 rounded-2xl bg-ttb08-card p-4 ring-1 ring-white/8 sm:grid-cols-[210px_1fr]">
      <div role="radiogroup" aria-label="Settings sections" class="relative flex flex-col [--i:0] has-[#ttb08-r2:checked]:[--i:1] has-[#ttb08-r3:checked]:[--i:2] has-[#ttb08-r4:checked]:[--i:3]">
        <span aria-hidden="true" class="absolute top-6 bottom-6 left-[15px] w-0.5 rounded-full bg-ttb08-acc/20"></span>
        <span aria-hidden="true" class="absolute top-6 left-[15px] h-[calc((100%-48px)*var(--i)/3)] w-0.5 rounded-full bg-ttb08-acc transition-[height] duration-500 ease-[cubic-bezier(0.65,0,0.35,1)] motion-reduce:transition-none"></span>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r1" checked><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Profile</span><span class="block text-[11px] text-ttb08-dim">Name, photo, bio</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r2"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Account</span><span class="block text-[11px] text-ttb08-dim">Email, password, 2FA</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r3"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Billing</span><span class="block text-[11px] text-ttb08-dim">Plan, invoices</span></span></label>
        <label class="group/tab relative flex h-12 cursor-pointer items-center gap-3 rounded-lg px-2.5 transition-colors hover:bg-ttb08-acc/6 has-focus-visible:outline-1 has-focus-visible:outline-offset-1 has-focus-visible:outline-dashed has-focus-visible:outline-ttb08-acc"><input class="sr-only" type="radio" name="ttb08" id="ttb08-r4"><span aria-hidden="true" class="relative z-10 size-3 flex-none rounded-full border-[1.5px] border-ttb08-acc/50 bg-ttb08-card transition-[background-color,border-color,box-shadow] group-has-checked/tab:border-ttb08-acc group-has-checked/tab:bg-ttb08-acc group-has-checked/tab:shadow-[0_0_10px_color-mix(in_oklab,var(--color-ttb08-acc)_60%,transparent)]"></span><span class="leading-tight"><span class="block text-[13px] font-semibold text-ttb08-mut transition-colors group-has-checked/tab:text-ttb08-acc-ink">Logs</span><span class="block text-[11px] text-ttb08-dim">Sessions, activity</span></span></label>
      </div>
      <div class="rounded-xl bg-white/[0.03] p-5 ring-1 ring-white/6">
        <div class="hidden group-has-[#ttb08-r1:checked]/tabs:block"><h3 class="text-lg font-bold">Profile</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">How you appear to teammates: display name, avatar and a short bio.</p></div>
        <div class="hidden group-has-[#ttb08-r2:checked]/tabs:block"><h3 class="text-lg font-bold">Account</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Sign-in email, password and two-factor authentication.</p></div>
        <div class="hidden group-has-[#ttb08-r3:checked]/tabs:block"><h3 class="text-lg font-bold">Billing</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Your current plan, payment method and downloadable invoices.</p></div>
        <div class="hidden group-has-[#ttb08-r4:checked]/tabs:block"><h3 class="text-lg font-bold">Logs</h3><p class="mt-1.5 text-sm leading-relaxed text-ttb08-mut">Active sessions and a 90-day record of account activity.</p></div>
      </div>
    </div>
    <p aria-hidden="true" class="rounded-full border border-white/10 px-3.5 py-2 font-ttb08-mono text-[11.5px] text-ttb08-mut">rail fill h-[calc((100%-48px)*var(--i)/3)] · group-has-checked/tab: dot</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-ttb08-bg: #0b0f15;
  --color-ttb08-bg: oklch(0.167 0.014 258.4);
  --color-ttb08-card: #111821;
  --color-ttb08-card: oklch(0.206 0.021 254.9);
  --color-ttb08-ink: #eef4fa;
  --color-ttb08-ink: oklch(0.964 0.010 247.9);
  --color-ttb08-mut: #a7b3c2;
  --color-ttb08-mut: oklch(0.762 0.025 254.0);
  --color-ttb08-dim: #8794a5;
  --color-ttb08-dim: oklch(0.662 0.029 255.1);
  --color-ttb08-acc: #38bdf8;
  --color-ttb08-acc: oklch(0.754 0.139 232.7);
  --color-ttb08-acc-ink: #7dd3fc;
  --color-ttb08-acc-ink: oklch(0.828 0.101 230.3);
  --font-ttb08-sans: 'Geist', system-ui, sans-serif;
  --font-ttb08-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-08 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Rail and fill share one geometry. Rows are 48px tall with no gap, so dot centres sit 24px from the top of the first row and the bottom of the last. The rail spans that distance; the fill covers i/3 of it:

<span class="absolute top-6 bottom-6 left-[15px] w-0.5 bg-ttb08-acc/20"></span>
<span class="absolute top-6 left-[15px] w-0.5 bg-ttb08-acc
  h-[calc((100%-48px)*var(--i)/3)] transition-[height]"></span>

No negative-margin arithmetic. The original pulled each dot onto the rail with margin-left:-29px; here the dot is the first flex child at a known x, so the rail is placed under it instead.

Make it yours

  • More steps: change the divisor from 3 to steps − 1 and add [--i] entries.
  • Light mode: stage #f8fafc, dot fill the stage colour so hollow dots punch the rail.
  • Turn the fill off for a pure nav (not a progress) by deleting the fill span.

Gotchas — read before shipping

  • The fill math assumes equal row heights — keep sub-labels to one line.
  • Hollow dots need a fill equal to the background or the rail shows through them.
  • For a true stepper (steps completed), style earlier dots with a counter or extra variants; this demo marks only the active one.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:has() drives the index; height transitions are universal.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…