23 / 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 Sliding Active Indicator Sidebar with CSS :target
The sliding active bar — the effect teams install a motion library for — with zero JavaScript. Links target anchors, :target identifies the section, and one indicator element transitions between stops computed from a single row-height variable.
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-23 w-full h-svh min-h-svh grid grid-cols-[224px_1fr] [--row:46px] font-tsl23-sans bg-tsl23-bg text-tsl23-ink">
<aside aria-label="Sections" class="relative bg-tsl23-panel border-r border-tsl23-line px-3 py-[18px]">
<span aria-hidden="true" class="tsl-23-ind absolute left-1 top-[18px] mt-2 w-[3px] h-7 rounded bg-tsl23-accent shadow-[0_0_12px_var(--color-tsl23-glow)] translate-y-0 transition-transform duration-[320ms] ease-[cubic-bezier(.5,0,.2,1.15)] motion-reduce:transition-none"></span>
<nav class="flex flex-col gap-0.5">
<a href="#tsl23-inbox" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Inbox</a>
<a href="#tsl23-today" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Today</a>
<a href="#tsl23-upcoming" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Upcoming</a>
<a href="#tsl23-someday" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Someday</a>
<a href="#tsl23-logbook" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Logbook</a>
</nav>
</aside>
<div class="overflow-y-auto p-8 grid gap-10 content-start [scrollbar-gutter:stable]">
<section id="tsl23-inbox" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Inbox</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Click the menu — the accent bar glides between items. URL fragments plus <code class="font-tsl23-mono text-[12.5px]">:target</code> and <code class="font-tsl23-mono text-[12.5px]">:has()</code>, no script.</p></section>
<section id="tsl23-today" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Today</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Three tasks scheduled.</p></section>
<section id="tsl23-upcoming" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Upcoming</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Planning horizon: two weeks.</p></section>
<section id="tsl23-someday" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Someday</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Ideas parked without guilt.</p></section>
<section id="tsl23-logbook" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Logbook</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Completed items, newest first.</p></section>
</div>
</section><section class="tsl-23 w-full h-svh min-h-svh grid grid-cols-[224px_1fr] [--row:46px] font-tsl23-sans bg-tsl23-bg text-tsl23-ink">
<aside aria-label="Sections" class="relative bg-tsl23-panel border-r border-tsl23-line px-3 py-[18px]">
<span aria-hidden="true" class="tsl-23-ind absolute left-1 top-[18px] mt-2 w-[3px] h-7 rounded bg-tsl23-accent shadow-[0_0_12px_var(--color-tsl23-glow)] translate-y-0 transition-transform duration-[320ms] ease-[cubic-bezier(.5,0,.2,1.15)] motion-reduce:transition-none"></span>
<nav class="flex flex-col gap-0.5">
<a href="#tsl23-inbox" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Inbox</a>
<a href="#tsl23-today" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Today</a>
<a href="#tsl23-upcoming" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Upcoming</a>
<a href="#tsl23-someday" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Someday</a>
<a href="#tsl23-logbook" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Logbook</a>
</nav>
</aside>
<div class="overflow-y-auto p-8 grid gap-10 content-start [scrollbar-gutter:stable]">
<section id="tsl23-inbox" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Inbox</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Click the menu — the accent bar glides between items. URL fragments plus <code class="font-tsl23-mono text-[12.5px]">:target</code> and <code class="font-tsl23-mono text-[12.5px]">:has()</code>, no script.</p></section>
<section id="tsl23-today" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Today</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Three tasks scheduled.</p></section>
<section id="tsl23-upcoming" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Upcoming</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Planning horizon: two weeks.</p></section>
<section id="tsl23-someday" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Someday</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Ideas parked without guilt.</p></section>
<section id="tsl23-logbook" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Logbook</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Completed items, newest first.</p></section>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}@import "tailwindcss";
@theme {
--color-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}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-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}@import "tailwindcss";
@theme {
--color-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}<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.
- A six-selector comma chain in the CSS original becomes one
has-[#id:target]:pair per link. The rule that highlights a link now lives on that link, which is the same win as demo 03 — and here it also removes the need to keep the chain in sync with the indicator stops. - UI change from the CSS original: rows are 44px exactly (the original's 44px height plus 2px gap is now the documented
--rowcontract), and the indicator's glow moved to a token so the shadow's commas stay readable.
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 Sliding Active Indicator Sidebar with CSS :target
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-sliding-active-indicator-sidebar-target/
The sliding active bar — the effect teams install a motion library for — with zero JavaScript. Links target anchors, :target identifies the section, and one indicator element transitions between stops computed from a single row-height variable.
## HTML
```html
<section class="tsl-23 w-full h-svh min-h-svh grid grid-cols-[224px_1fr] [--row:46px] font-tsl23-sans bg-tsl23-bg text-tsl23-ink">
<aside aria-label="Sections" class="relative bg-tsl23-panel border-r border-tsl23-line px-3 py-[18px]">
<span aria-hidden="true" class="tsl-23-ind absolute left-1 top-[18px] mt-2 w-[3px] h-7 rounded bg-tsl23-accent shadow-[0_0_12px_var(--color-tsl23-glow)] translate-y-0 transition-transform duration-[320ms] ease-[cubic-bezier(.5,0,.2,1.15)] motion-reduce:transition-none"></span>
<nav class="flex flex-col gap-0.5">
<a href="#tsl23-inbox" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Inbox</a>
<a href="#tsl23-today" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Today</a>
<a href="#tsl23-upcoming" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Upcoming</a>
<a href="#tsl23-someday" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Someday</a>
<a href="#tsl23-logbook" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Logbook</a>
</nav>
</aside>
<div class="overflow-y-auto p-8 grid gap-10 content-start [scrollbar-gutter:stable]">
<section id="tsl23-inbox" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Inbox</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Click the menu — the accent bar glides between items. URL fragments plus <code class="font-tsl23-mono text-[12.5px]">:target</code> and <code class="font-tsl23-mono text-[12.5px]">:has()</code>, no script.</p></section>
<section id="tsl23-today" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Today</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Three tasks scheduled.</p></section>
<section id="tsl23-upcoming" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Upcoming</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Planning horizon: two weeks.</p></section>
<section id="tsl23-someday" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Someday</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Ideas parked without guilt.</p></section>
<section id="tsl23-logbook" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Logbook</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Completed items, newest first.</p></section>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
```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 Sliding Active Indicator Sidebar with CSS :target
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-sliding-active-indicator-sidebar-target/
The sliding active bar — the effect teams install a motion library for — with zero JavaScript. Links target anchors, :target identifies the section, and one indicator element transitions between stops computed from a single row-height variable.
## HTML
```html
<section class="tsl-23 w-full h-svh min-h-svh grid grid-cols-[224px_1fr] [--row:46px] font-tsl23-sans bg-tsl23-bg text-tsl23-ink">
<aside aria-label="Sections" class="relative bg-tsl23-panel border-r border-tsl23-line px-3 py-[18px]">
<span aria-hidden="true" class="tsl-23-ind absolute left-1 top-[18px] mt-2 w-[3px] h-7 rounded bg-tsl23-accent shadow-[0_0_12px_var(--color-tsl23-glow)] translate-y-0 transition-transform duration-[320ms] ease-[cubic-bezier(.5,0,.2,1.15)] motion-reduce:transition-none"></span>
<nav class="flex flex-col gap-0.5">
<a href="#tsl23-inbox" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Inbox</a>
<a href="#tsl23-today" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Today</a>
<a href="#tsl23-upcoming" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Upcoming</a>
<a href="#tsl23-someday" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Someday</a>
<a href="#tsl23-logbook" class="flex items-center h-11 px-4 rounded-[10px] text-sm font-medium text-tsl23-mut no-underline transition-colors duration-200 hover:text-tsl23-ink hover:bg-tsl23-hover focus-visible:outline-2 focus-visible:outline-tsl23-accent focus-visible:outline-offset-2">Logbook</a>
</nav>
</aside>
<div class="overflow-y-auto p-8 grid gap-10 content-start [scrollbar-gutter:stable]">
<section id="tsl23-inbox" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Inbox</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Click the menu — the accent bar glides between items. URL fragments plus <code class="font-tsl23-mono text-[12.5px]">:target</code> and <code class="font-tsl23-mono text-[12.5px]">:has()</code>, no script.</p></section>
<section id="tsl23-today" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Today</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Three tasks scheduled.</p></section>
<section id="tsl23-upcoming" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Upcoming</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Planning horizon: two weeks.</p></section>
<section id="tsl23-someday" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Someday</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Ideas parked without guilt.</p></section>
<section id="tsl23-logbook" class="scroll-mt-6"><h1 class="mb-2 text-[21px] font-semibold leading-tight">Logbook</h1><p class="max-w-[48ch] text-sm leading-relaxed text-tsl23-mut text-pretty">Completed items, newest first.</p></section>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl23-bg: #101613;
--color-tsl23-bg: oklch(0.13 0.012 145);
--color-tsl23-panel: #161f1a;
--color-tsl23-panel: oklch(0.17 0.015 145);
--color-tsl23-ink: #e8f0ea;
--color-tsl23-ink: oklch(0.94 0.005 145);
--color-tsl23-mut: #929d95;
--color-tsl23-mut: oklch(0.64 0.012 145);
--color-tsl23-line: #333e37;
--color-tsl23-line: oklch(0.28 0.016 145);
--color-tsl23-accent: #67e29a;
--color-tsl23-accent: oklch(0.8 0.16 150);
--color-tsl23-glow: rgb(103 226 154 / 0.55);
--color-tsl23-glow: color-mix(in oklab, oklch(0.8 0.16 150) 55%, transparent);
--color-tsl23-hover: rgb(232 240 234 / 0.06);
--color-tsl23-hover: color-mix(in oklab, oklch(0.94 0.005 145) 6%, transparent);
--color-tsl23-tint: rgb(103 226 154 / 0.12);
--color-tsl23-tint: color-mix(in oklab, oklch(0.8 0.16 150) 12%, transparent);
--font-tsl23-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tsl23-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tsl-23 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* Nothing is :target on first paint — park the resting highlight on item one
so the default state looks intentional rather than empty. */
.tsl-23:not(:has(:target)) nav a:first-child {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
/* STATE — indicator stops and the matching highlight, both derived from --row. */
.tsl-23:has(#tsl23-today:target) .tsl-23-ind {
translate: 0 var(--row);
}
.tsl-23:has(#tsl23-upcoming:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 2);
}
.tsl-23:has(#tsl23-someday:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 3);
}
.tsl-23:has(#tsl23-logbook:target) .tsl-23-ind {
translate: 0 calc(var(--row) * 4);
}
.tsl-23:has(#tsl23-inbox:target) a[href="#tsl23-inbox"],
.tsl-23:has(#tsl23-today:target) a[href="#tsl23-today"],
.tsl-23:has(#tsl23-upcoming:target) a[href="#tsl23-upcoming"],
.tsl-23:has(#tsl23-someday:target) a[href="#tsl23-someday"],
.tsl-23:has(#tsl23-logbook:target) a[href="#tsl23-logbook"] {
color: var(--color-tsl23-ink);
background: var(--color-tsl23-tint);
}
```How this works
Fixed row height is the contract. Rows are exactly 44px with a 2px gap, so stop n is n × var(--row) — knowable constants rather than measured values. That is the honest limitation too: this pattern fits stable menus, while dynamic lists want demo 14's anchor positioning, which measures for you.
The default state needs stating. With no fragment in the URL, nothing is :target. one scoped :not(:has(:target)) rule in the stylesheet handles it — a negated relational variant would have to be repeated on every link to say the same thing.
Make it yours
- Extract
--rowand every stop follows; change row height in one place. - Horizontal variant: the same stops on
translate-xunder a tab bar. - Prefer a pill? Swap the 3px bar for a full-row rounded rect at low opacity behind the links.
Gotchas — read before shipping
- Every
:targetjump scrolls the content pane — that is the feature, but keep the pane in its own scroll context so the page does not jump too. - Ids are document-global; namespace them or two widgets will fight over the fragment.
- Handle the no-fragment state explicitly or the indicator is invisible until the first click.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() containing :target is the gate (Baseline Dec 2023). Without it links still highlight on :target; only the glide is lost.