Pure TailwindMIT licensed

Tailwind Step Progress Wizard Navbar

A checkout header: five steps, connector lines that fill behind completed ones, and a summary strip with a progress bar. Each step declares its own state as data-state="done|current|todo", and every visual — circle fill, label colour, connector — is a data-state-* variant, so advancing the flow means changing one attribute per step.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<div class="tnb-17 w-full min-h-svh bg-tnb17-page font-tnb17-sans text-tnb17-ink" style="--tnb17-progress:0.4" aria-label="Step progress wizard navbar demo">
  <header class="sticky top-0 z-100 border-b border-tnb17-edge bg-white shadow-[0_1px_4px_oklch(0_0_0/0.05)]">
    <div class="flex items-center justify-between border-b border-tnb17-edge px-5 py-3 md:px-8">
      <a href="#home" class="text-lg font-extrabold tracking-[-0.02em] text-tnb17-ink no-underline">Form<span class="text-tnb17-acc">Flow</span></a>
      <a href="#help" class="flex items-center gap-2 text-sm font-bold text-tnb17-mut no-underline transition-colors hover:text-tnb17-ink"><span aria-hidden="true">&#128172;</span>Need help?</a>
    </div>
    <ol class="flex items-center overflow-x-auto px-5 py-4 [scrollbar-width:none] md:px-8" aria-label="Checkout steps">
      <li data-state="done" class="peer/s1 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="done" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=done]:border-tnb17-done data-[state=done]:bg-tnb17-done data-[state=done]:text-white" aria-hidden="true">&#10003;</span>
        <span class="grid max-sm:hidden"><span data-state="done" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=done]:text-tnb17-done">Step 1</span><span class="text-sm font-bold text-tnb17-ink">Account</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s1:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="done" class="peer/s2 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="done" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=done]:border-tnb17-done data-[state=done]:bg-tnb17-done data-[state=done]:text-white" aria-hidden="true">&#10003;</span>
        <span class="grid max-sm:hidden"><span data-state="done" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=done]:text-tnb17-done">Step 2</span><span class="text-sm font-bold text-tnb17-ink">Profile</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s2:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="current" aria-current="step" class="peer/s3 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="current" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=current]:border-tnb17-acc data-[state=current]:bg-tnb17-acc data-[state=current]:text-white">3</span>
        <span class="grid max-sm:hidden"><span data-state="current" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=current]:text-tnb17-acc">Step 3</span><span class="text-sm font-bold text-tnb17-ink">Payment</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s3:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="todo" class="peer/s4 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut">4</span>
        <span class="grid max-sm:hidden"><span class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase">Step 4</span><span class="text-sm font-bold text-tnb17-mut">Review</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s4:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="todo" class="flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut">5</span>
        <span class="grid max-sm:hidden"><span class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase">Step 5</span><span class="text-sm font-bold text-tnb17-mut">Confirm</span></span>
      </li>
    </ol>
    <div class="flex items-center justify-between gap-4 border-t border-tnb17-acc/10 bg-tnb17-tint px-5 py-2.5 md:px-8">
      <p class="text-[13px] font-bold whitespace-nowrap text-tnb17-acc">Step 3 of 5 &mdash; Payment details</p>
      <div class="h-1.5 max-w-50 flex-1 overflow-hidden rounded-sm bg-tnb17-acc/15" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" aria-label="Checkout progress">
        <div class="h-full origin-left scale-x-[var(--tnb17-progress)] rounded-sm bg-tnb17-acc transition-transform duration-500 motion-reduce:transition-none"></div>
      </div>
      <button type="button" class="cursor-pointer border-0 bg-transparent text-[13px] font-bold whitespace-nowrap text-tnb17-mut transition-colors hover:text-tnb17-ink">Save &amp; exit</button>
    </div>
  </header>
  <main class="mx-auto max-w-xl px-5 py-12 md:px-8">
    <h1 class="mb-4 text-3xl font-extrabold tracking-[-0.03em] text-tnb17-ink">Step Progress Wizard</h1>
    <p class="text-lg leading-relaxed text-tnb17-mut">Each step owns its state as a <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">data-state</code> attribute; the connector after it fills only when that step reads <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">done</code>, via <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">peer-data-[state=done]:</code>.</p>
  </main>
</div>
@import "tailwindcss";

@theme {
  --color-tnb17-page: #f7f8fb;
  --color-tnb17-page: oklch(0.98 0.004 275);
  --color-tnb17-ink: #1e2337;
  --color-tnb17-ink: oklch(0.27 0.03 275);
  --color-tnb17-mut: #6f7793;
  --color-tnb17-mut: oklch(0.57 0.03 275);
  --color-tnb17-edge: #e5e8f0;
  --color-tnb17-edge: oklch(0.92 0.008 275);
  --color-tnb17-tint: #f0eeff;
  --color-tnb17-tint: oklch(0.95 0.02 285);
  --color-tnb17-acc: #5b4ef8;
  --color-tnb17-acc: oklch(0.53 0.24 283);
  --color-tnb17-done: #18a957;
  --color-tnb17-done: oklch(0.6 0.16 150);
  --font-tnb17-sans: 'Nunito Sans', system-ui, sans-serif;
}

.tnb-17 {
  width: 100%;
  min-height: 100svh;
}

.tnb-17 ::selection {
  background: var(--color-tnb17-acc);
  color: #fff;
}

.tnb-17 ol::-webkit-scrollbar {
  display: none;
}

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-tnb17-page: #f7f8fb;
  --color-tnb17-page: oklch(0.98 0.004 275);
  --color-tnb17-ink: #1e2337;
  --color-tnb17-ink: oklch(0.27 0.03 275);
  --color-tnb17-mut: #6f7793;
  --color-tnb17-mut: oklch(0.57 0.03 275);
  --color-tnb17-edge: #e5e8f0;
  --color-tnb17-edge: oklch(0.92 0.008 275);
  --color-tnb17-tint: #f0eeff;
  --color-tnb17-tint: oklch(0.95 0.02 285);
  --color-tnb17-acc: #5b4ef8;
  --color-tnb17-acc: oklch(0.53 0.24 283);
  --color-tnb17-done: #18a957;
  --color-tnb17-done: oklch(0.6 0.16 150);
  --font-tnb17-sans: 'Nunito Sans', system-ui, sans-serif;
}

.tnb-17 {
  width: 100%;
  min-height: 100svh;
}

.tnb-17 ::selection {
  background: var(--color-tnb17-acc);
  color: #fff;
}

.tnb-17 ol::-webkit-scrollbar {
  display: none;
}
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=Nunito+Sans:wght@400;600;700;800&display=swap">

What changed in the port

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

  • .is-done / .is-active classes became one data-state attribute with three values, so a step cannot be marked done and active at once.
  • The connector's .is-done + .connector::after rule becomes peer-data-[state=done]/sN:after:scale-x-100 — named peers, because five connectors each watch a different step, and the fill stays a pseudo-element so the sibling combinator can reach it.
  • The summary bar switched from an animated width to scale-x-[var(--tnb17-progress)], matching demo 08's approach.
  • The green was darkened to oklch(0.6 0.16 150) so the completed-step label clears 4.5:1 on white; the original's #22c55e did not.
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 Step Progress Wizard Navbar
Source: https://codefronts.com/navigation/tailwind-css-responsive-navbars/tailwind-step-progress-wizard-navbar/

A checkout header: five steps, connector lines that fill behind completed ones, and a summary strip with a progress bar. Each step declares its own state as data-state="done|current|todo", and every visual — circle fill, label colour, connector — is a data-state-* variant, so advancing the flow means changing one attribute per step.
## HTML
```html
<div class="tnb-17 w-full min-h-svh bg-tnb17-page font-tnb17-sans text-tnb17-ink" style="--tnb17-progress:0.4" aria-label="Step progress wizard navbar demo">
  <header class="sticky top-0 z-100 border-b border-tnb17-edge bg-white shadow-[0_1px_4px_oklch(0_0_0/0.05)]">
    <div class="flex items-center justify-between border-b border-tnb17-edge px-5 py-3 md:px-8">
      <a href="#home" class="text-lg font-extrabold tracking-[-0.02em] text-tnb17-ink no-underline">Form<span class="text-tnb17-acc">Flow</span></a>
      <a href="#help" class="flex items-center gap-2 text-sm font-bold text-tnb17-mut no-underline transition-colors hover:text-tnb17-ink"><span aria-hidden="true">&#128172;</span>Need help?</a>
    </div>
    <ol class="flex items-center overflow-x-auto px-5 py-4 [scrollbar-width:none] md:px-8" aria-label="Checkout steps">
      <li data-state="done" class="peer/s1 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="done" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=done]:border-tnb17-done data-[state=done]:bg-tnb17-done data-[state=done]:text-white" aria-hidden="true">&#10003;</span>
        <span class="grid max-sm:hidden"><span data-state="done" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=done]:text-tnb17-done">Step 1</span><span class="text-sm font-bold text-tnb17-ink">Account</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s1:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="done" class="peer/s2 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="done" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=done]:border-tnb17-done data-[state=done]:bg-tnb17-done data-[state=done]:text-white" aria-hidden="true">&#10003;</span>
        <span class="grid max-sm:hidden"><span data-state="done" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=done]:text-tnb17-done">Step 2</span><span class="text-sm font-bold text-tnb17-ink">Profile</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s2:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="current" aria-current="step" class="peer/s3 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span data-state="current" class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut transition-colors duration-300 data-[state=current]:border-tnb17-acc data-[state=current]:bg-tnb17-acc data-[state=current]:text-white">3</span>
        <span class="grid max-sm:hidden"><span data-state="current" class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase data-[state=current]:text-tnb17-acc">Step 3</span><span class="text-sm font-bold text-tnb17-ink">Payment</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s3:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="todo" class="peer/s4 flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut">4</span>
        <span class="grid max-sm:hidden"><span class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase">Step 4</span><span class="text-sm font-bold text-tnb17-mut">Review</span></span>
      </li>
      <span class="relative mx-1 h-0.5 min-w-8 flex-1 rounded-sm bg-tnb17-edge after:absolute after:inset-0 after:origin-left after:scale-x-0 after:rounded-sm after:bg-tnb17-done after:transition-transform after:duration-500 after:content-[''] peer-data-[state=done]/s4:after:scale-x-100 motion-reduce:after:transition-none sm:max-w-20"></span>
      <li data-state="todo" class="flex shrink-0 items-center gap-3 rounded-[10px] px-3 py-1.5">
        <span class="grid size-8 shrink-0 place-items-center rounded-full border-2 border-tnb17-edge text-xs font-extrabold text-tnb17-mut">5</span>
        <span class="grid max-sm:hidden"><span class="text-[11px] font-bold tracking-[0.04em] text-tnb17-mut uppercase">Step 5</span><span class="text-sm font-bold text-tnb17-mut">Confirm</span></span>
      </li>
    </ol>
    <div class="flex items-center justify-between gap-4 border-t border-tnb17-acc/10 bg-tnb17-tint px-5 py-2.5 md:px-8">
      <p class="text-[13px] font-bold whitespace-nowrap text-tnb17-acc">Step 3 of 5 &mdash; Payment details</p>
      <div class="h-1.5 max-w-50 flex-1 overflow-hidden rounded-sm bg-tnb17-acc/15" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" aria-label="Checkout progress">
        <div class="h-full origin-left scale-x-[var(--tnb17-progress)] rounded-sm bg-tnb17-acc transition-transform duration-500 motion-reduce:transition-none"></div>
      </div>
      <button type="button" class="cursor-pointer border-0 bg-transparent text-[13px] font-bold whitespace-nowrap text-tnb17-mut transition-colors hover:text-tnb17-ink">Save &amp; exit</button>
    </div>
  </header>
  <main class="mx-auto max-w-xl px-5 py-12 md:px-8">
    <h1 class="mb-4 text-3xl font-extrabold tracking-[-0.03em] text-tnb17-ink">Step Progress Wizard</h1>
    <p class="text-lg leading-relaxed text-tnb17-mut">Each step owns its state as a <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">data-state</code> attribute; the connector after it fills only when that step reads <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">done</code>, via <code class="rounded bg-tnb17-tint px-1.5 py-0.5 text-[0.9em] text-tnb17-acc">peer-data-[state=done]:</code>.</p>
  </main>
</div>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tnb17-page: #f7f8fb;
  --color-tnb17-page: oklch(0.98 0.004 275);
  --color-tnb17-ink: #1e2337;
  --color-tnb17-ink: oklch(0.27 0.03 275);
  --color-tnb17-mut: #6f7793;
  --color-tnb17-mut: oklch(0.57 0.03 275);
  --color-tnb17-edge: #e5e8f0;
  --color-tnb17-edge: oklch(0.92 0.008 275);
  --color-tnb17-tint: #f0eeff;
  --color-tnb17-tint: oklch(0.95 0.02 285);
  --color-tnb17-acc: #5b4ef8;
  --color-tnb17-acc: oklch(0.53 0.24 283);
  --color-tnb17-done: #18a957;
  --color-tnb17-done: oklch(0.6 0.16 150);
  --font-tnb17-sans: 'Nunito Sans', system-ui, sans-serif;
}

.tnb-17 {
  width: 100%;
  min-height: 100svh;
}

.tnb-17 ::selection {
  background: var(--color-tnb17-acc);
  color: #fff;
}

.tnb-17 ol::-webkit-scrollbar {
  display: none;
}
```

How this works

One attribute per step, three consumers. The circle, the label and the following connector all read the same value:

<li data-state="done" class="peer …
  data-[state=done]:[&_.circle]:bg-tnb17-done">

In practice the demo puts the variants directly on the circle and label elements, which keeps each one self-describing: data-[state=current]:border-tnb17-acc on the circle, data-[state=done]:text-tnb17-done on the label.

The connector belongs to the gap. It is a peer-following element whose own after: strip is the fill, so peer-data-[state=done]/sN:after:scale-x-100 fills it only when the step before it is complete — no :last-child exception needed, because the last step has no connector after it in the markup.

Narrow screens keep the circles. Labels hide with max-sm:hidden and the connectors shorten; the flow stays legible at 360px without a second component.

Make it yours

  • Step count: add a step and a connector pair; nothing is indexed, so no renumbering.
  • Animate the fill: the connector already transitions — set duration-500 for a slow sweep as a step completes.
  • Progress bar: --tnb17-progress on the root, from 0 to 1.
  • Clickable steps: wrap completed step content in an <a>; data-[state=todo]:pointer-events-none keeps future steps inert.

Gotchas — read before shipping

  • peer reaches later siblings only — never their descendants. The fill must therefore be the connector’s own after: strip, not a child element, or the rule silently never matches.
  • Do not encode state in two places (a class and an attribute) — the attribute is the one assistive tech can read via aria-current.
  • Horizontal step flows overflow fast; the demo scrolls the row rather than wrapping, because a wrapped wizard reads as two wizards.
  • A checkmark glyph inside the circle should be aria-hidden — the state is already announced by aria-current and the label.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Nothing exotic; the floor quoted is Tailwind v4's generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…