08 / 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 Pure CSS Collapsible Sidebar Drawer without JavaScript
The zero-JS drawer done the 2026 way: a hidden checkbox holds state, has-checked: reads it from any depth, and the panel slides on a GPU-composited transform. The hamburger morphs into an X from the same signal, using nth-child arbitrary variants.
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-08 relative w-full h-svh min-h-svh grid grid-rows-[auto_1fr] overflow-hidden font-tsl08-sans bg-tsl08-bg text-tsl08-ink">
<input type="checkbox" id="tsl08-t" class="sr-only peer">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl08-panel border-b border-tsl08-line text-[15px] font-semibold">
<label for="tsl08-t" aria-hidden="true" class="grid gap-[4.5px] -ml-2.5 px-2.5 py-3 rounded-[9px] cursor-pointer">
<span class="tsl-08-b1 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
<span class="tsl-08-b2 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-opacity duration-200"></span>
<span class="tsl-08-b3 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
</label>
<strong>Fieldnotes</strong>
</header>
<label for="tsl08-t" aria-hidden="true" class="tsl-08-scrim absolute inset-x-0 bottom-0 top-[54px] z-10 bg-[oklch(0_0_0/0.5)] backdrop-blur-[2px] opacity-0 pointer-events-none transition-opacity duration-300 motion-reduce:transition-none"></label>
<aside aria-label="Menu" class="tsl-08-drawer absolute left-0 top-[54px] bottom-0 z-20 w-[min(280px,85vw)] px-3 py-4 bg-tsl08-panel border-r border-tsl08-line -translate-x-full invisible transition-[translate,visibility] duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] delay-0 starting:-translate-x-full motion-reduce:transition-none [transition-behavior:allow-discrete]">
<nav class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover aria-[current=page]:text-tsl08-ink aria-[current=page]:bg-tsl08-tint focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">All notes</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Notebooks</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Tags</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Archive</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Trash</a>
</nav>
</aside>
<main class="p-7 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-2.5">All notes</h1>
<p class="max-w-[46ch] text-sm leading-relaxed text-tsl08-mut text-pretty">Open the drawer — the only animated property is transform, so the slide is fully GPU-composited. Zero JavaScript on this page.</p>
</main>
</section><section class="tsl-08 relative w-full h-svh min-h-svh grid grid-rows-[auto_1fr] overflow-hidden font-tsl08-sans bg-tsl08-bg text-tsl08-ink">
<input type="checkbox" id="tsl08-t" class="sr-only peer">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl08-panel border-b border-tsl08-line text-[15px] font-semibold">
<label for="tsl08-t" aria-hidden="true" class="grid gap-[4.5px] -ml-2.5 px-2.5 py-3 rounded-[9px] cursor-pointer">
<span class="tsl-08-b1 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
<span class="tsl-08-b2 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-opacity duration-200"></span>
<span class="tsl-08-b3 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
</label>
<strong>Fieldnotes</strong>
</header>
<label for="tsl08-t" aria-hidden="true" class="tsl-08-scrim absolute inset-x-0 bottom-0 top-[54px] z-10 bg-[oklch(0_0_0/0.5)] backdrop-blur-[2px] opacity-0 pointer-events-none transition-opacity duration-300 motion-reduce:transition-none"></label>
<aside aria-label="Menu" class="tsl-08-drawer absolute left-0 top-[54px] bottom-0 z-20 w-[min(280px,85vw)] px-3 py-4 bg-tsl08-panel border-r border-tsl08-line -translate-x-full invisible transition-[translate,visibility] duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] delay-0 starting:-translate-x-full motion-reduce:transition-none [transition-behavior:allow-discrete]">
<nav class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover aria-[current=page]:text-tsl08-ink aria-[current=page]:bg-tsl08-tint focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">All notes</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Notebooks</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Tags</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Archive</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Trash</a>
</nav>
</aside>
<main class="p-7 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-2.5">All notes</h1>
<p class="max-w-[46ch] text-sm leading-relaxed text-tsl08-mut text-pretty">Open the drawer — the only animated property is transform, so the slide is fully GPU-composited. Zero JavaScript on this page.</p>
</main>
</section>@import "tailwindcss";
@theme {
--color-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}@import "tailwindcss";
@theme {
--color-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}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-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}@import "tailwindcss";
@theme {
--color-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}<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.
- This is the demo where the port stops being a translation and starts being a judgement call. The resting design is 100% utilities; the state machine is five scoped lines of CSS, because a checkbox at the shell root is an ANCESTOR of nothing and a sibling of everything.
- The asymmetric
visibilitydelay is the one thing that stayed CSS. Tailwind can expressdelay-[320ms], but not two different delays for two properties in one declaration — a two-line rule is honest here. - UI change from the CSS original: drawer links are now
min-h-11rather than 11px of padding, and the drawer carriestransition-behavior:allow-discreteso the visibility swap interpolates properly in Chrome rather than snapping a frame early.
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 Pure CSS Collapsible Sidebar Drawer without JavaScript
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-pure-css-collapsible-sidebar-drawer-no-javascript/
The zero-JS drawer done the 2026 way: a hidden checkbox holds state, has-checked: reads it from any depth, and the panel slides on a GPU-composited transform. The hamburger morphs into an X from the same signal, using nth-child arbitrary variants.
## HTML
```html
<section class="tsl-08 relative w-full h-svh min-h-svh grid grid-rows-[auto_1fr] overflow-hidden font-tsl08-sans bg-tsl08-bg text-tsl08-ink">
<input type="checkbox" id="tsl08-t" class="sr-only peer">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl08-panel border-b border-tsl08-line text-[15px] font-semibold">
<label for="tsl08-t" aria-hidden="true" class="grid gap-[4.5px] -ml-2.5 px-2.5 py-3 rounded-[9px] cursor-pointer">
<span class="tsl-08-b1 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
<span class="tsl-08-b2 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-opacity duration-200"></span>
<span class="tsl-08-b3 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
</label>
<strong>Fieldnotes</strong>
</header>
<label for="tsl08-t" aria-hidden="true" class="tsl-08-scrim absolute inset-x-0 bottom-0 top-[54px] z-10 bg-[oklch(0_0_0/0.5)] backdrop-blur-[2px] opacity-0 pointer-events-none transition-opacity duration-300 motion-reduce:transition-none"></label>
<aside aria-label="Menu" class="tsl-08-drawer absolute left-0 top-[54px] bottom-0 z-20 w-[min(280px,85vw)] px-3 py-4 bg-tsl08-panel border-r border-tsl08-line -translate-x-full invisible transition-[translate,visibility] duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] delay-0 starting:-translate-x-full motion-reduce:transition-none [transition-behavior:allow-discrete]">
<nav class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover aria-[current=page]:text-tsl08-ink aria-[current=page]:bg-tsl08-tint focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">All notes</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Notebooks</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Tags</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Archive</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Trash</a>
</nav>
</aside>
<main class="p-7 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-2.5">All notes</h1>
<p class="max-w-[46ch] text-sm leading-relaxed text-tsl08-mut text-pretty">Open the drawer — the only animated property is transform, so the slide is fully GPU-composited. Zero JavaScript on this page.</p>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}
```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 Pure CSS Collapsible Sidebar Drawer without JavaScript
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-pure-css-collapsible-sidebar-drawer-no-javascript/
The zero-JS drawer done the 2026 way: a hidden checkbox holds state, has-checked: reads it from any depth, and the panel slides on a GPU-composited transform. The hamburger morphs into an X from the same signal, using nth-child arbitrary variants.
## HTML
```html
<section class="tsl-08 relative w-full h-svh min-h-svh grid grid-rows-[auto_1fr] overflow-hidden font-tsl08-sans bg-tsl08-bg text-tsl08-ink">
<input type="checkbox" id="tsl08-t" class="sr-only peer">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl08-panel border-b border-tsl08-line text-[15px] font-semibold">
<label for="tsl08-t" aria-hidden="true" class="grid gap-[4.5px] -ml-2.5 px-2.5 py-3 rounded-[9px] cursor-pointer">
<span class="tsl-08-b1 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
<span class="tsl-08-b2 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-opacity duration-200"></span>
<span class="tsl-08-b3 block w-[19px] h-0.5 rounded-sm bg-tsl08-ink transition-[rotate,translate,opacity] duration-300 motion-reduce:transition-none"></span>
</label>
<strong>Fieldnotes</strong>
</header>
<label for="tsl08-t" aria-hidden="true" class="tsl-08-scrim absolute inset-x-0 bottom-0 top-[54px] z-10 bg-[oklch(0_0_0/0.5)] backdrop-blur-[2px] opacity-0 pointer-events-none transition-opacity duration-300 motion-reduce:transition-none"></label>
<aside aria-label="Menu" class="tsl-08-drawer absolute left-0 top-[54px] bottom-0 z-20 w-[min(280px,85vw)] px-3 py-4 bg-tsl08-panel border-r border-tsl08-line -translate-x-full invisible transition-[translate,visibility] duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] delay-0 starting:-translate-x-full motion-reduce:transition-none [transition-behavior:allow-discrete]">
<nav class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover aria-[current=page]:text-tsl08-ink aria-[current=page]:bg-tsl08-tint focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">All notes</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Notebooks</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Tags</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Archive</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl08-mut no-underline transition-colors duration-200 hover:text-tsl08-ink hover:bg-tsl08-hover focus-visible:outline-2 focus-visible:outline-tsl08-accent focus-visible:outline-offset-2">Trash</a>
</nav>
</aside>
<main class="p-7 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-2.5">All notes</h1>
<p class="max-w-[46ch] text-sm leading-relaxed text-tsl08-mut text-pretty">Open the drawer — the only animated property is transform, so the slide is fully GPU-composited. Zero JavaScript on this page.</p>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl08-bg: #16121b;
--color-tsl08-bg: oklch(0.14 0.015 300);
--color-tsl08-panel: #1d1824;
--color-tsl08-panel: oklch(0.18 0.02 300);
--color-tsl08-ink: #eae7ef;
--color-tsl08-ink: oklch(0.93 0.005 300);
--color-tsl08-mut: #9b95a4;
--color-tsl08-mut: oklch(0.64 0.012 300);
--color-tsl08-line: #3c3545;
--color-tsl08-line: oklch(0.29 0.02 300);
--color-tsl08-accent: #d07ae0;
--color-tsl08-accent: oklch(0.72 0.16 320);
--color-tsl08-hover: rgb(234 231 239 / 0.07);
--color-tsl08-hover: color-mix(in oklab, oklch(0.93 0.005 300) 7%, transparent);
--color-tsl08-tint: rgb(208 122 224 / 0.18);
--color-tsl08-tint: color-mix(in oklab, oklch(0.72 0.16 320) 18%, transparent);
--font-tsl08-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-08 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — one checkbox, four consequences: drawer, scrim and the three bars. */
#tsl08-t:checked ~ .tsl-08-drawer {
translate: 0;
visibility: visible;
}
#tsl08-t:checked ~ .tsl-08-scrim {
opacity: 1;
pointer-events: auto;
}
#tsl08-t:checked ~ header .tsl-08-b1 {
rotate: 45deg;
translate: 0 6.5px;
}
#tsl08-t:checked ~ header .tsl-08-b2 {
opacity: 0;
}
#tsl08-t:checked ~ header .tsl-08-b3 {
rotate: -45deg;
translate: 0 -6.5px;
}
#tsl08-t:focus-visible ~ header label[for="tsl08-t"] {
outline: 2px solid var(--color-tsl08-accent);
outline-offset: 2px;
}
/* visibility must lag the slide on close and lead it on open — one property
with two different timings, which no single utility can express. */
.tsl-08 aside {
transition-delay: 0s, 320ms;
}
.tsl-08:has(#tsl08-t:checked) aside {
transition-delay: 0s, 0s;
}
```How this works
One state signal, four consequences. The drawer, the scrim and the three hamburger bars are all SIBLINGS of the checkbox, so no variant reaches them — has-checked: needs the input inside the element, and peer-* cannot descend into the header to find the bars. Five lines of #tsl08-t:checked ~ … do the whole state machine, and every resting value stays a utility. The CSS original needed five descendant selectors rooted at the section; here nothing is rooted anywhere — each element reads the checkbox itself.
Performance is the point of the pattern. Only transform and opacity animate, so the slide is fully compositor-side. visibility rides along with a transition delay on close, which keeps hidden links out of the tab order without the display-toggle that would kill the animation.
Make it yours
- Right-hand drawer:
translate-x-fullas the resting state andright-0. - Spring feel:
ease-[linear(0,1.2_60%,1)]— CSSlinear()easing gives a real overshoot. - Width:
w-[min(280px,85vw)]never swallows a small screen.
Gotchas — read before shipping
has-checked:matches only when the checkbox is INSIDE that element — on the shell it works, on a sibling drawer it silently never matches. Everything below the root reads the input by id instead:[:has(#tsl08-t:checked)_&]:translate-x-0.- A transform-hidden drawer is still in the tab order; the
invisiblepair is not optional. - Scrim-as-label is a click target, not a button. Esc-to-close needs four lines of JS — document the trade-off rather than pretending it is covered.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 117+ | 17.4+ | 129+ | 117+ |
Floor set by :has(), oklch(), color-mix(), transition-behavior as this demo is written. The core technique itself goes back further — Chrome 111+.
:has() is the only real gate — every state variant here compiles to it. Where :has() is unavailable, a sibling-combinator port (input immediately before the drawer, peer-checked:) runs a decade back.