07 / 20
All 20 in 20 Tailwind CSS Sidebars
- 01Tailwind Accessible Sidebar Navigation ARIA Reference
- 02Tailwind Sidebar Focus Trap and Restore on Close
- 03Tailwind Inert Background Content Behind a Drawer
- 04Tailwind Sidebar Navigation Is Not a Tab List
- 05Tailwind Announcing Collapsed and Expanded Sidebar State
- 06Tailwind Roving Tabindex for Sidebar Keyboard Navigation
- 07Tailwind Persisting Sidebar Collapse State Across Reloads
- 08Tailwind Sidebar Scroll Containment with Overscroll Behavior
- 09Tailwind Nested Submenu Keyboard and Screen Reader Behavior
- 10Tailwind Resizable Sidebar with a Keyboard Accessible Handle
- 11Pure Tailwind Collapsible Sidebar with Checkbox State
- 12Tailwind Responsive Sidebar Breakpoint Behavior
- 13Pure Tailwind Icon Rail Sidebar with Accessible Labels
- 14Tailwind Dashboard Sidebar with Current Page State
- 15Pure Tailwind Multi Level Sidebar Menu Disclosure Pattern
- 16Pure Tailwind Accordion Sidebar with Details and Summary
- 17Pure Tailwind Sticky Full Height Sidebar Layout Mechanics
- 18Pure Tailwind Floating Sidebar Stacking and Overlay Order
- 19Tailwind Off Canvas Sidebar Drawer with Focus Return
- 20Pure Tailwind Sidebar Footer Pinning with Scrollable Nav
Tailwind Persisting Sidebar Collapse State Across Reloads
A collapse toggle the user has to re-click on every reload is a toggle that does not work. This one writes to localStorage and restores on load — and the detail that makes it feel native is the second attribute: data-noanim, read by group-data-noanim:transition-none, which suppresses the width transition for exactly one frame so the restored rail appears already collapsed instead of animating into place.
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="tsb-07 block relative w-full min-h-svh bg-tsb07-bg text-tsb07-ink font-tsb07-sans leading-[1.55]" aria-label="Prism analytics">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-07-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-07-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb07-line bg-tsb07-surf text-tsb07-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb07-acc peer-focus-visible:outline-offset-2" for="tsb-07-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="group flex min-w-0 min-h-svh bg-tsb07-surf max-r30:flex-col" data-collapsed="false" data-noanim>
<aside class="flex-none flex flex-col min-w-0 w-[var(--tsb07-w-open)] bg-tsb07-surf2 border-r border-tsb07-line transition-[width] duration-200 ease-out group-data-[collapsed=true]:w-[var(--tsb07-w-rail)] group-data-noanim:transition-none motion-reduce:transition-none max-r30:w-full max-r30:border-r-0 max-r30:border-b max-r30:group-data-[collapsed=true]:w-full" id="tsb-07-rail">
<div class="flex items-center gap-2 px-2.5 py-2 pr-14 border-b border-tsb07-line group-data-[collapsed=true]:flex-col group-data-[collapsed=true]:gap-1 group-data-[collapsed=true]:px-0 group-data-[collapsed=true]:py-1.5 r30:pr-2.5">
<span class="flex-none grid place-items-center w-7.5 h-7.5 bg-tsb07-acc text-tsb07-onacc text-[0.7rem] font-bold tracking-[0.06em]" aria-hidden="true">PR</span>
<p class="mr-auto m-0 min-w-0 overflow-hidden font-tsb07-display text-[0.82rem] tracking-[0.2em] uppercase group-data-[collapsed=true]:mr-0 group-data-[collapsed=true]:flex-none"><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Prism</span></p>
<button class="flex-none grid place-items-center w-11 h-11 border-0 bg-transparent text-tsb07-ink cursor-pointer [&>svg]:w-4.5 [&>svg]:h-4.5 focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" type="button" id="tsb-07-toggle" aria-expanded="true" aria-controls="tsb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]" data-tsb-07-label>Collapse sidebar</span>
</button>
</div>
<nav class="min-w-0 px-1.5 py-1.5" aria-label="Prism reports">
<ul class="list-none m-0 p-0">
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb07-accsoft aria-[current=page]:text-tsb07-acc aria-[current=page]:font-semibold aria-[current=page]:[&>span:first-child]:text-tsb07-acc" href="#tsb-07-main" aria-current="page"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">01</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Overview</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">02</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Acquisition</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">03</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Retention</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">04</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Revenue</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">05</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="flex-1 min-w-0 grid gap-5 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-07-main">
<div class="grid grid-cols-[minmax(0,1fr)] gap-3 r34:grid-cols-3">
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Active workspaces</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">1,284</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Weekly retention</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">71.4%</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Net revenue</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">$418k</p></div>
</div>
<div class="flex items-end gap-1.5 h-48 p-3 border border-tsb07-line [&>span]:flex-1 [&>span]:min-w-0 [&>span]:bg-tsb07-acc [&>span]:opacity-75 [&>span]:h-[calc(var(--tsb07-h)*1%)]" aria-hidden="true">
<span style="--tsb07-h:32"></span><span style="--tsb07-h:48"></span><span style="--tsb07-h:41"></span>
<span style="--tsb07-h:66"></span><span style="--tsb07-h:58"></span><span style="--tsb07-h:74"></span>
<span style="--tsb07-h:69"></span><span style="--tsb07-h:88"></span><span style="--tsb07-h:79"></span>
</div>
</main>
</div>
</section><section class="tsb-07 block relative w-full min-h-svh bg-tsb07-bg text-tsb07-ink font-tsb07-sans leading-[1.55]" aria-label="Prism analytics">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-07-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-07-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb07-line bg-tsb07-surf text-tsb07-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb07-acc peer-focus-visible:outline-offset-2" for="tsb-07-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="group flex min-w-0 min-h-svh bg-tsb07-surf max-r30:flex-col" data-collapsed="false" data-noanim>
<aside class="flex-none flex flex-col min-w-0 w-[var(--tsb07-w-open)] bg-tsb07-surf2 border-r border-tsb07-line transition-[width] duration-200 ease-out group-data-[collapsed=true]:w-[var(--tsb07-w-rail)] group-data-noanim:transition-none motion-reduce:transition-none max-r30:w-full max-r30:border-r-0 max-r30:border-b max-r30:group-data-[collapsed=true]:w-full" id="tsb-07-rail">
<div class="flex items-center gap-2 px-2.5 py-2 pr-14 border-b border-tsb07-line group-data-[collapsed=true]:flex-col group-data-[collapsed=true]:gap-1 group-data-[collapsed=true]:px-0 group-data-[collapsed=true]:py-1.5 r30:pr-2.5">
<span class="flex-none grid place-items-center w-7.5 h-7.5 bg-tsb07-acc text-tsb07-onacc text-[0.7rem] font-bold tracking-[0.06em]" aria-hidden="true">PR</span>
<p class="mr-auto m-0 min-w-0 overflow-hidden font-tsb07-display text-[0.82rem] tracking-[0.2em] uppercase group-data-[collapsed=true]:mr-0 group-data-[collapsed=true]:flex-none"><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Prism</span></p>
<button class="flex-none grid place-items-center w-11 h-11 border-0 bg-transparent text-tsb07-ink cursor-pointer [&>svg]:w-4.5 [&>svg]:h-4.5 focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" type="button" id="tsb-07-toggle" aria-expanded="true" aria-controls="tsb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]" data-tsb-07-label>Collapse sidebar</span>
</button>
</div>
<nav class="min-w-0 px-1.5 py-1.5" aria-label="Prism reports">
<ul class="list-none m-0 p-0">
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb07-accsoft aria-[current=page]:text-tsb07-acc aria-[current=page]:font-semibold aria-[current=page]:[&>span:first-child]:text-tsb07-acc" href="#tsb-07-main" aria-current="page"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">01</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Overview</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">02</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Acquisition</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">03</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Retention</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">04</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Revenue</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">05</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="flex-1 min-w-0 grid gap-5 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-07-main">
<div class="grid grid-cols-[minmax(0,1fr)] gap-3 r34:grid-cols-3">
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Active workspaces</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">1,284</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Weekly retention</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">71.4%</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Net revenue</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">$418k</p></div>
</div>
<div class="flex items-end gap-1.5 h-48 p-3 border border-tsb07-line [&>span]:flex-1 [&>span]:min-w-0 [&>span]:bg-tsb07-acc [&>span]:opacity-75 [&>span]:h-[calc(var(--tsb07-h)*1%)]" aria-hidden="true">
<span style="--tsb07-h:32"></span><span style="--tsb07-h:48"></span><span style="--tsb07-h:41"></span>
<span style="--tsb07-h:66"></span><span style="--tsb07-h:58"></span><span style="--tsb07-h:74"></span>
<span style="--tsb07-h:69"></span><span style="--tsb07-h:88"></span><span style="--tsb07-h:79"></span>
</div>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}@import "tailwindcss";
@theme {
--color-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}(function () {
document.querySelectorAll('.tsb-07').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var app = root.querySelector('.group'),
btn = root.querySelector('#tsb-07-toggle'),
label = btn.querySelector('[data-tsb-07-label]'),
KEY = 'tsb-07:rail-collapsed';
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
function write(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
function paint(collapsed) {
app.setAttribute('data-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
label.textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head>, so the restored width is
// correct in the first frame and data-noanim is not needed at all.
app.setAttribute('data-noanim', '');
paint(read() === 'true');
requestAnimationFrame(function () { app.removeAttribute('data-noanim'); });
function toggle() {
var collapsed = app.getAttribute('data-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__tsbCleanup = function () { btn.removeEventListener('click', toggle); };
});
})();(function () {
document.querySelectorAll('.tsb-07').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var app = root.querySelector('.group'),
btn = root.querySelector('#tsb-07-toggle'),
label = btn.querySelector('[data-tsb-07-label]'),
KEY = 'tsb-07:rail-collapsed';
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
function write(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
function paint(collapsed) {
app.setAttribute('data-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
label.textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head>, so the restored width is
// correct in the first frame and data-noanim is not needed at all.
app.setAttribute('data-noanim', '');
paint(read() === 'true');
requestAnimationFrame(function () { app.removeAttribute('data-noanim'); });
function toggle() {
var collapsed = app.getAttribute('data-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__tsbCleanup = function () { btn.removeEventListener('click', toggle); };
});
})();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-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}@import "tailwindcss";
@theme {
--color-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}<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=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&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=Archivo:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- This demo is the clearest illustration of the two data-variant forms:
group-data-[collapsed=true]:because that attribute carries a value,group-data-noanim:because that one is presence-only. Both are on the same group, three characters apart in the class list. [data-noanim] .rail { transition: none }ports to a single utility on the rail, which is a real gain: the suppression now sits next to the transition it suppresses.- The collapsed head reflow — a documented workaround in the CSS version, written as a late rule so it would win on source order — becomes three ordinary utilities. Variant order in Tailwind is resolved by the compiler, so there is no source-order trap to comment about.
- Bar heights use the per-item value pattern:
style="--tsb07-h:48"read by[&>span]:h-[calc(var(--tsb07-h)*1%)]on the chart. Nine<span>elements, one utility, no re-indexing. - The active row's numeral recolour is
aria-[current=page]:[&>span:first-child]:text-tsb07-acc— a nested arbitrary selector under a state variant, which is where utilities are at their least readable and still worth it for keeping the state in one place. localStoragestays entirely in script, wrapped intry/catch. Nothing about persistence belongs in the class list.
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 Persisting Sidebar Collapse State Across Reloads
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-persisting-sidebar-collapse-state-across-reloads/
A collapse toggle the user has to re-click on every reload is a toggle that does not work. This one writes to localStorage and restores on load — and the detail that makes it feel native is the second attribute: data-noanim, read by group-data-noanim:transition-none, which suppresses the width transition for exactly one frame so the restored rail appears already collapsed instead of animating into place.
## HTML
```html
<section class="tsb-07 block relative w-full min-h-svh bg-tsb07-bg text-tsb07-ink font-tsb07-sans leading-[1.55]" aria-label="Prism analytics">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-07-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-07-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb07-line bg-tsb07-surf text-tsb07-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb07-acc peer-focus-visible:outline-offset-2" for="tsb-07-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="group flex min-w-0 min-h-svh bg-tsb07-surf max-r30:flex-col" data-collapsed="false" data-noanim>
<aside class="flex-none flex flex-col min-w-0 w-[var(--tsb07-w-open)] bg-tsb07-surf2 border-r border-tsb07-line transition-[width] duration-200 ease-out group-data-[collapsed=true]:w-[var(--tsb07-w-rail)] group-data-noanim:transition-none motion-reduce:transition-none max-r30:w-full max-r30:border-r-0 max-r30:border-b max-r30:group-data-[collapsed=true]:w-full" id="tsb-07-rail">
<div class="flex items-center gap-2 px-2.5 py-2 pr-14 border-b border-tsb07-line group-data-[collapsed=true]:flex-col group-data-[collapsed=true]:gap-1 group-data-[collapsed=true]:px-0 group-data-[collapsed=true]:py-1.5 r30:pr-2.5">
<span class="flex-none grid place-items-center w-7.5 h-7.5 bg-tsb07-acc text-tsb07-onacc text-[0.7rem] font-bold tracking-[0.06em]" aria-hidden="true">PR</span>
<p class="mr-auto m-0 min-w-0 overflow-hidden font-tsb07-display text-[0.82rem] tracking-[0.2em] uppercase group-data-[collapsed=true]:mr-0 group-data-[collapsed=true]:flex-none"><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Prism</span></p>
<button class="flex-none grid place-items-center w-11 h-11 border-0 bg-transparent text-tsb07-ink cursor-pointer [&>svg]:w-4.5 [&>svg]:h-4.5 focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" type="button" id="tsb-07-toggle" aria-expanded="true" aria-controls="tsb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]" data-tsb-07-label>Collapse sidebar</span>
</button>
</div>
<nav class="min-w-0 px-1.5 py-1.5" aria-label="Prism reports">
<ul class="list-none m-0 p-0">
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb07-accsoft aria-[current=page]:text-tsb07-acc aria-[current=page]:font-semibold aria-[current=page]:[&>span:first-child]:text-tsb07-acc" href="#tsb-07-main" aria-current="page"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">01</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Overview</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">02</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Acquisition</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">03</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Retention</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">04</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Revenue</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">05</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="flex-1 min-w-0 grid gap-5 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-07-main">
<div class="grid grid-cols-[minmax(0,1fr)] gap-3 r34:grid-cols-3">
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Active workspaces</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">1,284</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Weekly retention</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">71.4%</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Net revenue</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">$418k</p></div>
</div>
<div class="flex items-end gap-1.5 h-48 p-3 border border-tsb07-line [&>span]:flex-1 [&>span]:min-w-0 [&>span]:bg-tsb07-acc [&>span]:opacity-75 [&>span]:h-[calc(var(--tsb07-h)*1%)]" aria-hidden="true">
<span style="--tsb07-h:32"></span><span style="--tsb07-h:48"></span><span style="--tsb07-h:41"></span>
<span style="--tsb07-h:66"></span><span style="--tsb07-h:58"></span><span style="--tsb07-h:74"></span>
<span style="--tsb07-h:69"></span><span style="--tsb07-h:88"></span><span style="--tsb07-h:79"></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tsb-07').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var app = root.querySelector('.group'),
btn = root.querySelector('#tsb-07-toggle'),
label = btn.querySelector('[data-tsb-07-label]'),
KEY = 'tsb-07:rail-collapsed';
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
function write(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
function paint(collapsed) {
app.setAttribute('data-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
label.textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head>, so the restored width is
// correct in the first frame and data-noanim is not needed at all.
app.setAttribute('data-noanim', '');
paint(read() === 'true');
requestAnimationFrame(function () { app.removeAttribute('data-noanim'); });
function toggle() {
var collapsed = app.getAttribute('data-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__tsbCleanup = function () { btn.removeEventListener('click', toggle); };
});
})();
```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 Persisting Sidebar Collapse State Across Reloads
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-persisting-sidebar-collapse-state-across-reloads/
A collapse toggle the user has to re-click on every reload is a toggle that does not work. This one writes to localStorage and restores on load — and the detail that makes it feel native is the second attribute: data-noanim, read by group-data-noanim:transition-none, which suppresses the width transition for exactly one frame so the restored rail appears already collapsed instead of animating into place.
## HTML
```html
<section class="tsb-07 block relative w-full min-h-svh bg-tsb07-bg text-tsb07-ink font-tsb07-sans leading-[1.55]" aria-label="Prism analytics">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-07-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-07-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb07-line bg-tsb07-surf text-tsb07-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb07-acc peer-focus-visible:outline-offset-2" for="tsb-07-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="group flex min-w-0 min-h-svh bg-tsb07-surf max-r30:flex-col" data-collapsed="false" data-noanim>
<aside class="flex-none flex flex-col min-w-0 w-[var(--tsb07-w-open)] bg-tsb07-surf2 border-r border-tsb07-line transition-[width] duration-200 ease-out group-data-[collapsed=true]:w-[var(--tsb07-w-rail)] group-data-noanim:transition-none motion-reduce:transition-none max-r30:w-full max-r30:border-r-0 max-r30:border-b max-r30:group-data-[collapsed=true]:w-full" id="tsb-07-rail">
<div class="flex items-center gap-2 px-2.5 py-2 pr-14 border-b border-tsb07-line group-data-[collapsed=true]:flex-col group-data-[collapsed=true]:gap-1 group-data-[collapsed=true]:px-0 group-data-[collapsed=true]:py-1.5 r30:pr-2.5">
<span class="flex-none grid place-items-center w-7.5 h-7.5 bg-tsb07-acc text-tsb07-onacc text-[0.7rem] font-bold tracking-[0.06em]" aria-hidden="true">PR</span>
<p class="mr-auto m-0 min-w-0 overflow-hidden font-tsb07-display text-[0.82rem] tracking-[0.2em] uppercase group-data-[collapsed=true]:mr-0 group-data-[collapsed=true]:flex-none"><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Prism</span></p>
<button class="flex-none grid place-items-center w-11 h-11 border-0 bg-transparent text-tsb07-ink cursor-pointer [&>svg]:w-4.5 [&>svg]:h-4.5 focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" type="button" id="tsb-07-toggle" aria-expanded="true" aria-controls="tsb-07-rail">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 12h10M4 18h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]" data-tsb-07-label>Collapse sidebar</span>
</button>
</div>
<nav class="min-w-0 px-1.5 py-1.5" aria-label="Prism reports">
<ul class="list-none m-0 p-0">
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb07-accsoft aria-[current=page]:text-tsb07-acc aria-[current=page]:font-semibold aria-[current=page]:[&>span:first-child]:text-tsb07-acc" href="#tsb-07-main" aria-current="page"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">01</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Overview</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">02</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Acquisition</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">03</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Retention</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">04</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Revenue</span></a></li>
<li><a class="flex items-center gap-2.5 min-w-0 min-h-11 px-2.5 py-2 text-[0.9rem] text-tsb07-ink no-underline whitespace-nowrap hover:bg-tsb07-surf focus-visible:outline-2 focus-visible:outline-tsb07-acc focus-visible:-outline-offset-2" href="#tsb-07-main"><span class="flex-none w-6 font-tsb07-mono text-[0.72rem] text-tsb07-mut" aria-hidden="true">05</span><span class="min-w-0 overflow-hidden text-ellipsis group-data-[collapsed=true]:absolute group-data-[collapsed=true]:w-px group-data-[collapsed=true]:h-px group-data-[collapsed=true]:whitespace-nowrap group-data-[collapsed=true]:[clip-path:inset(50%)]">Audit log</span></a></li>
</ul>
</nav>
</aside>
<main class="flex-1 min-w-0 grid gap-5 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-07-main">
<div class="grid grid-cols-[minmax(0,1fr)] gap-3 r34:grid-cols-3">
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Active workspaces</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">1,284</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Weekly retention</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">71.4%</p></div>
<div class="min-w-0 px-4 py-3.5 border border-tsb07-line"><p class="m-0 mb-1 text-[0.7rem] tracking-[0.12em] uppercase text-tsb07-mut">Net revenue</p><p class="m-0 font-tsb07-display text-[1.45rem] tabular-nums">$418k</p></div>
</div>
<div class="flex items-end gap-1.5 h-48 p-3 border border-tsb07-line [&>span]:flex-1 [&>span]:min-w-0 [&>span]:bg-tsb07-acc [&>span]:opacity-75 [&>span]:h-[calc(var(--tsb07-h)*1%)]" aria-hidden="true">
<span style="--tsb07-h:32"></span><span style="--tsb07-h:48"></span><span style="--tsb07-h:41"></span>
<span style="--tsb07-h:66"></span><span style="--tsb07-h:58"></span><span style="--tsb07-h:74"></span>
<span style="--tsb07-h:69"></span><span style="--tsb07-h:88"></span><span style="--tsb07-h:79"></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb07-bg: #eef4f1;
--color-tsb07-bg: oklch(95.5% 0.012 172);
--color-tsb07-surf: #ffffff;
--color-tsb07-surf: oklch(100% 0 0);
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-surf2: oklch(92.5% 0.016 172);
--color-tsb07-line: #cbd8d2;
--color-tsb07-line: oklch(86% 0.02 172);
--color-tsb07-ink: #16211d;
--color-tsb07-ink: oklch(21% 0.018 172);
--color-tsb07-mut: #5a6a64;
--color-tsb07-mut: oklch(47% 0.02 172);
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-accsoft: oklch(91% 0.03 172);
--color-tsb07-onacc: #ffffff;
--color-tsb07-onacc: oklch(100% 0 0);
--font-tsb07-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb07-display: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-tsb07-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--breakpoint-r30: 30rem;
--breakpoint-r34: 34rem;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the two rail widths, which are geometry read
by w-[var(...)] rather than a colour scale. */
.tsb-07 {
width: 100%;
min-height: 100svh;
--tsb07-w-open: 14.5rem;
--tsb07-w-rail: 4.25rem;
}
.tsb-07 *,
.tsb-07 *::before,
.tsb-07 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
@media (prefers-color-scheme: dark) {
.tsb-07 {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #eef4f1;
--color-tsb07-surf: #ffffff;
--color-tsb07-surf2: #e2ebe7;
--color-tsb07-line: #cbd8d2;
--color-tsb07-ink: #16211d;
--color-tsb07-mut: #5a6a64;
--color-tsb07-acc: #0f7a63;
--color-tsb07-acc: oklch(50% 0.1 172);
--color-tsb07-accsoft: #d5eae4;
--color-tsb07-onacc: #ffffff;
}
}
@media (prefers-color-scheme: light) {
.tsb-07:has(.tsb-07-theme-input:checked) {
--color-tsb07-bg: #0b1210;
--color-tsb07-surf: #121a18;
--color-tsb07-surf2: #18221f;
--color-tsb07-line: #26332f;
--color-tsb07-ink: #e9f1ee;
--color-tsb07-mut: #93a4a0;
--color-tsb07-acc: #62d9b8;
--color-tsb07-acc: oklch(82% 0.12 172);
--color-tsb07-accsoft: #12302a;
--color-tsb07-onacc: #0b1210;
}
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tsb-07').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var app = root.querySelector('.group'),
btn = root.querySelector('#tsb-07-toggle'),
label = btn.querySelector('[data-tsb-07-label]'),
KEY = 'tsb-07:rail-collapsed';
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
function write(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
function paint(collapsed) {
app.setAttribute('data-collapsed', String(collapsed));
btn.setAttribute('aria-expanded', String(!collapsed));
label.textContent = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
}
// In production this runs as an inline script in <head>, so the restored width is
// correct in the first frame and data-noanim is not needed at all.
app.setAttribute('data-noanim', '');
paint(read() === 'true');
requestAnimationFrame(function () { app.removeAttribute('data-noanim'); });
function toggle() {
var collapsed = app.getAttribute('data-collapsed') !== 'true';
write(String(collapsed));
paint(collapsed);
}
btn.addEventListener('click', toggle);
root.__tsbCleanup = function () { btn.removeEventListener('click', toggle); };
});
})();
```How this works
Two pieces of state, one of them temporary. data-collapsed is the real state, persisted. data-noanim exists for a single frame:
<aside class="transition-[width] duration-200
group-data-[collapsed=true]:w-[var(--tsb07-w-rail)]
group-data-noanim:transition-none">The script sets data-noanim, paints the restored state, then removes it inside requestAnimationFrame. Without it, every reload plays a 200ms collapse animation the user did not ask for — the tell of a restored-state bug.
Both variant forms on one group, deliberately. data-collapsed carries true/false, so it needs the bracketed value form. data-noanim is presence-only, so it takes the boolean form. Seeing the two side by side is the clearest possible illustration of when each applies.
Storage access is wrapped, always. localStorage throws in Safari private mode and under some cookie policies. Every read and write is in a try/catch, and the fallback is the default expanded state — a sidebar that forgets is fine; a sidebar that throws on load is not.
The collapsed head had to be restacked, not just narrowed. At 4.25rem the mark, the gap and a 44px button do not fit on one row, so the collapsed state switches the head to a column. That is three utilities in the class list, and it is visible next to the row layout it replaces.
Make it yours
- Change the storage key in one place at the top of the script; namespacing it per product is the only change most apps need.
- Rail widths:
--tsb07-w-openand--tsb07-w-railin the root rule. - Move persistence to a cookie or a user record if the preference should follow the account rather than the device; the paint function is already the only writer.
- In production, run the restore as an inline script in
<head>so the width is correct in the very first frame anddata-noanimis not needed at all. - Retheme through
--color-tsb07-acc; the mark, the active row, the chart bars and the focus rings read it. - Swap the numeral icons for real glyphs by replacing the
<span>content; the 1.5rem reserved width keeps the rows aligned either way.
Gotchas — read before shipping
- Restoring state without suppressing the transition animates the sidebar on every page load. It is the single most common polish bug in this pattern.
- Remove
data-noaniminsiderequestAnimationFrame, not withsetTimeout(0)— a timeout can land after the browser has already painted, and you get the animation anyway. localStoragecan throw on access, not just on write. Wrap the read too.aria-expandedmust be restored alongside the width, or the first paint looks collapsed and announces expanded.group-data-noanim:andgroup-data-[collapsed=true]:are not interchangeable forms — the boolean form on afalse-valued attribute matches, because the attribute is still present.- Hiding labels with
hiddeninstead of the visually hidden utilities silently removes every accessible name in the collapsed rail.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4's generated sheet sets the floor. localStorage has no floor and the one-frame suppression is plain requestAnimationFrame; where storage is blocked the demo falls back to the expanded default.