26 / 30
All 30 in 30 Tailwind CSS Sidebar Layouts
- 01Tailwind SaaS Dashboard Sidebar with Native Dark Mode Switch
- 02Tailwind Collapsible Admin Sidebar with Icon Tooltips
- 03Tailwind Documentation Sidebar with Multi-Level :target Tree
- 04Tailwind Responsive Sidebar to Bottom Navigation Switch Layout
- 05Tailwind Analytics Sidebar with CSS Subgrid Column Alignment
- 06Tailwind Animated Collapsible Sidebar with Icon-Only Mode
- 07Tailwind Sticky Sidebar Layout with Scroll-Driven Progress Bar
- 08Tailwind Pure CSS Collapsible Sidebar Drawer without JavaScript
- 09Tailwind Fixed Sidebar Layout with Independently Scrolling Content
- 10Tailwind Off-Canvas Navigation Drawer with the Popover API
- 11Tailwind Right-Aligned Sidebar Layout for Inspector Panels
- 12Tailwind Two-Pane Webmail Layout with List and Reading Pane
- 13Tailwind Dual Sidebar App Shell with Left Nav and Context Panel
- 14Tailwind Mini Icon Rail Sidebar with Anchor-Positioned Active Pill
- 15Tailwind E-Commerce Filter Sidebar with Native Details Accordion
- 16Tailwind Floating Glassmorphism Sidebar with Gradient Hairline
- 17Tailwind Sidebar Menu with Pinned User Profile Footer
- 18Tailwind Push-Content Sidebar Drawer with GPU Transitions
- 19Tailwind Off-Canvas Cart Overlay with Blurred Backdrop Scrim
- 20Tailwind Sidebar with Persistent Collapsed State in localStorage
- 21Tailwind Sidebar with Sticky Search Bar Inside the Rail
- 22Tailwind Section-Grouped Sidebar Navigation with Header Labels
- 23Tailwind Sliding Active Indicator Sidebar with CSS :target
- 24Tailwind Multi-Level Nested Tree Sidebar with Details Branches
- 25Tailwind Tabbed Sidebar with Radio-Driven Panel Switching
- 26Tailwind Onboarding Wizard Sidebar with Step Progress Tracking
- 27Tailwind Resizable Sidebar Layout Using the CSS Resize Property
- 28Tailwind Three-Pane Workspace Shell with Icon Rail and List
- 29Tailwind Adaptive Sidebar Layout Using CSS Container Queries
- 30Tailwind Frosted Glass Floating Sidebar with Animated Aurora
Tailwind Onboarding Wizard Sidebar with Step Progress Tracking
The setup-wizard rail: done / current / upcoming steps, checkmark badges, a connector that fills with accent behind you, and CSS counters numbering the pending steps automatically. Add or remove a step and numbering, line and states all recompute.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="tsl-26 w-full h-svh min-h-svh grid grid-cols-[300px_1fr] font-tsl26-sans bg-tsl26-bg text-tsl26-ink">
<aside aria-label="Setup progress" class="bg-tsl26-panel border-r border-tsl26-line px-[22px] py-[26px] overflow-y-auto [scrollbar-gutter:stable]">
<h2 class="mb-6 text-[15px] font-bold tracking-tight">Workspace setup</h2>
<ol class="tsl-26-steps list-none m-0 p-0 flex flex-col [counter-reset:tsl26]">
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Create account</strong><small class="text-xs leading-[1.45] text-tsl26-mut">ada@nimbus.io verified</small></div>
</li>
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Invite your team</strong><small class="text-xs leading-[1.45] text-tsl26-mut">4 teammates joined</small></div>
</li>
<li data-state="current" aria-current="step" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-white text-xs font-bold text-tsl26-accent shadow-[0_0_0_4px_var(--color-tsl26-halo)] before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-accent">Connect data source</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Postgres, BigQuery or CSV</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Build first dashboard</strong><small class="text-xs leading-[1.45] text-tsl26-mut">From a template or scratch</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Set up alerts</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Slack or email digests</small></div>
</li>
</ol>
</aside>
<main class="p-9 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="mb-2.5 text-[22px] font-bold leading-tight">Connect data source</h1>
<p class="max-w-[52ch] mb-[22px] text-sm leading-[1.65] text-tsl26-mut text-pretty">The rail is an ordered list with three data-states — counters number the todos, checkmarks replace done numbers, and the connector fills behind you.</p>
<button type="button" class="min-h-12 px-[22px] rounded-[11px] border-0 bg-tsl26-accent text-white text-sm font-semibold cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tsl26-accent focus-visible:outline-offset-[3px]">Connect Postgres</button>
</main>
</section><section class="tsl-26 w-full h-svh min-h-svh grid grid-cols-[300px_1fr] font-tsl26-sans bg-tsl26-bg text-tsl26-ink">
<aside aria-label="Setup progress" class="bg-tsl26-panel border-r border-tsl26-line px-[22px] py-[26px] overflow-y-auto [scrollbar-gutter:stable]">
<h2 class="mb-6 text-[15px] font-bold tracking-tight">Workspace setup</h2>
<ol class="tsl-26-steps list-none m-0 p-0 flex flex-col [counter-reset:tsl26]">
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Create account</strong><small class="text-xs leading-[1.45] text-tsl26-mut">ada@nimbus.io verified</small></div>
</li>
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Invite your team</strong><small class="text-xs leading-[1.45] text-tsl26-mut">4 teammates joined</small></div>
</li>
<li data-state="current" aria-current="step" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-white text-xs font-bold text-tsl26-accent shadow-[0_0_0_4px_var(--color-tsl26-halo)] before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-accent">Connect data source</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Postgres, BigQuery or CSV</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Build first dashboard</strong><small class="text-xs leading-[1.45] text-tsl26-mut">From a template or scratch</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Set up alerts</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Slack or email digests</small></div>
</li>
</ol>
</aside>
<main class="p-9 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="mb-2.5 text-[22px] font-bold leading-tight">Connect data source</h1>
<p class="max-w-[52ch] mb-[22px] text-sm leading-[1.65] text-tsl26-mut text-pretty">The rail is an ordered list with three data-states — counters number the todos, checkmarks replace done numbers, and the connector fills behind you.</p>
<button type="button" class="min-h-12 px-[22px] rounded-[11px] border-0 bg-tsl26-accent text-white text-sm font-semibold cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tsl26-accent focus-visible:outline-offset-[3px]">Connect Postgres</button>
</main>
</section>@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">
<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Counters port surprisingly well:
[counter-increment:tsl26]andbefore:content-[counter(tsl26)]are legible arbitrary values, and putting the increment on the list item makes the mechanism obvious for once. - The masked checkmark is the exception. A data-URI SVG carries quotes, angle brackets and commas — hostile to arbitrary-value parsing — so it stays a CSS rule. Inlining a real
<svg>in the badge is the alternative if you want zero custom CSS. - UI change from the CSS original: the primary button is
min-h-12, and todo-step titles use the muted token at 4.6:1 rather than inheriting a lighter mix.
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 Onboarding Wizard Sidebar with Step Progress Tracking
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-onboarding-wizard-sidebar-step-progress/
The setup-wizard rail: done / current / upcoming steps, checkmark badges, a connector that fills with accent behind you, and CSS counters numbering the pending steps automatically. Add or remove a step and numbering, line and states all recompute.
## HTML
```html
<section class="tsl-26 w-full h-svh min-h-svh grid grid-cols-[300px_1fr] font-tsl26-sans bg-tsl26-bg text-tsl26-ink">
<aside aria-label="Setup progress" class="bg-tsl26-panel border-r border-tsl26-line px-[22px] py-[26px] overflow-y-auto [scrollbar-gutter:stable]">
<h2 class="mb-6 text-[15px] font-bold tracking-tight">Workspace setup</h2>
<ol class="tsl-26-steps list-none m-0 p-0 flex flex-col [counter-reset:tsl26]">
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Create account</strong><small class="text-xs leading-[1.45] text-tsl26-mut">ada@nimbus.io verified</small></div>
</li>
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Invite your team</strong><small class="text-xs leading-[1.45] text-tsl26-mut">4 teammates joined</small></div>
</li>
<li data-state="current" aria-current="step" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-white text-xs font-bold text-tsl26-accent shadow-[0_0_0_4px_var(--color-tsl26-halo)] before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-accent">Connect data source</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Postgres, BigQuery or CSV</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Build first dashboard</strong><small class="text-xs leading-[1.45] text-tsl26-mut">From a template or scratch</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Set up alerts</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Slack or email digests</small></div>
</li>
</ol>
</aside>
<main class="p-9 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="mb-2.5 text-[22px] font-bold leading-tight">Connect data source</h1>
<p class="max-w-[52ch] mb-[22px] text-sm leading-[1.65] text-tsl26-mut text-pretty">The rail is an ordered list with three data-states — counters number the todos, checkmarks replace done numbers, and the connector fills behind you.</p>
<button type="button" class="min-h-12 px-[22px] rounded-[11px] border-0 bg-tsl26-accent text-white text-sm font-semibold cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tsl26-accent focus-visible:outline-offset-[3px]">Connect Postgres</button>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}
```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 Onboarding Wizard Sidebar with Step Progress Tracking
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-onboarding-wizard-sidebar-step-progress/
The setup-wizard rail: done / current / upcoming steps, checkmark badges, a connector that fills with accent behind you, and CSS counters numbering the pending steps automatically. Add or remove a step and numbering, line and states all recompute.
## HTML
```html
<section class="tsl-26 w-full h-svh min-h-svh grid grid-cols-[300px_1fr] font-tsl26-sans bg-tsl26-bg text-tsl26-ink">
<aside aria-label="Setup progress" class="bg-tsl26-panel border-r border-tsl26-line px-[22px] py-[26px] overflow-y-auto [scrollbar-gutter:stable]">
<h2 class="mb-6 text-[15px] font-bold tracking-tight">Workspace setup</h2>
<ol class="tsl-26-steps list-none m-0 p-0 flex flex-col [counter-reset:tsl26]">
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Create account</strong><small class="text-xs leading-[1.45] text-tsl26-mut">ada@nimbus.io verified</small></div>
</li>
<li data-state="done" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0 data-[state=done]:after:bg-tsl26-accent">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-tsl26-accent text-xs font-bold"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug">Invite your team</strong><small class="text-xs leading-[1.45] text-tsl26-mut">4 teammates joined</small></div>
</li>
<li data-state="current" aria-current="step" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-accent bg-white text-xs font-bold text-tsl26-accent shadow-[0_0_0_4px_var(--color-tsl26-halo)] before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-accent">Connect data source</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Postgres, BigQuery or CSV</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Build first dashboard</strong><small class="text-xs leading-[1.45] text-tsl26-mut">From a template or scratch</small></div>
</li>
<li data-state="todo" class="relative grid grid-cols-[28px_1fr] gap-3.5 pb-7 [counter-increment:tsl26] after:content-[''] after:absolute after:left-[13.5px] after:top-8 after:bottom-1 after:w-0.5 after:rounded-sm after:bg-tsl26-line after:z-0 last:after:hidden last:pb-0">
<span aria-hidden="true" class="tsl-26-badge relative z-[1] grid place-items-center w-7 h-7 rounded-full border-2 border-tsl26-line bg-white text-xs font-bold text-tsl26-mut before:content-[counter(tsl26)]"></span>
<div><strong class="block text-[13.5px] font-semibold leading-snug text-tsl26-mut">Set up alerts</strong><small class="text-xs leading-[1.45] text-tsl26-mut">Slack or email digests</small></div>
</li>
</ol>
</aside>
<main class="p-9 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="mb-2.5 text-[22px] font-bold leading-tight">Connect data source</h1>
<p class="max-w-[52ch] mb-[22px] text-sm leading-[1.65] text-tsl26-mut text-pretty">The rail is an ordered list with three data-states — counters number the todos, checkmarks replace done numbers, and the connector fills behind you.</p>
<button type="button" class="min-h-12 px-[22px] rounded-[11px] border-0 bg-tsl26-accent text-white text-sm font-semibold cursor-pointer transition-[filter] duration-200 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-tsl26-accent focus-visible:outline-offset-[3px]">Connect Postgres</button>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl26-bg: #fcfcfe;
--color-tsl26-bg: oklch(0.99 0.002 275);
--color-tsl26-panel: #f3f3f7;
--color-tsl26-panel: oklch(0.965 0.006 275);
--color-tsl26-ink: #3a3b46;
--color-tsl26-ink: oklch(0.26 0.02 275);
--color-tsl26-mut: #6f7180;
--color-tsl26-mut: oklch(0.5 0.015 275);
--color-tsl26-line: #dcdce3;
--color-tsl26-line: oklch(0.88 0.008 275);
--color-tsl26-accent: #5b4bd6;
--color-tsl26-accent: oklch(0.55 0.18 285);
--color-tsl26-halo: rgb(91 75 214 / 0.18);
--color-tsl26-halo: color-mix(in oklab, oklch(0.55 0.18 285) 18%, transparent);
--font-tsl26-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-26 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: light;
}
/* A masked checkmark replacing generated content: the mask URL is a data
URI with its own quotes and commas, which no arbitrary value survives. */
.tsl-26 li[data-state="done"] .tsl-26-badge::before {
content: "";
width: 12px;
height: 12px;
background: #fff;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.5 5.5 5.5L20 6.5"/></svg>') center/contain no-repeat;
}
```How this works
Three states and one counter generate the whole rail, which is why it survives edits that break hand-built wizards. data-[state=…]: variants style badge, connector and text from a single attribute per step; the counter numbers the pending ones; done steps replace their number with a masked checkmark.
The current step gets a halo — shadow-[0_0_0_4px_var(--color-tsl26-halo)] — because in a five-step list the eye needs one unambiguous focal point, and colour alone in the text is too quiet.
Make it yours
- Steps are data: map state per step server-side; the utilities handle any count.
- Let users jump back by wrapping done steps' text in links; leave todo steps as plain text so nobody skips ahead.
- Sub-steps: a nested
<ol>with its own[counter-reset:…]namespaces automatically.
Gotchas — read before shipping
aria-current="step"belongs on ONE element. Duplicates make a screen reader announce several current steps.- The connector needs the item
relativeand the badge above it (z-[1]), or the line strikes through the checkmark. - Never encode done-ness in colour alone — the checkmark replacing the number is the second signal.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
CSS counters are ancient; color-mix() and the masked checkmark set the practical floor, and an inline SVG is the fallback.