18 / 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 Push-Content Sidebar Drawer with GPU Transitions
The drawer that pushes instead of covering: opening slides the entire canvas right by exactly the drawer's width, both elements moving as one welded unit. One shared variable feeds the drawer width and the canvas displacement, so they cannot drift.
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-18 relative w-full h-svh min-h-svh overflow-hidden [--w:280px] font-tsl18-sans bg-tsl18-bg text-tsl18-ink">
<input type="checkbox" id="tsl18-p" class="sr-only peer">
<aside aria-label="Layers" class="tsl-18-drawer absolute inset-y-0 left-0 z-[2] w-[var(--w)] px-3.5 py-5 bg-tsl18-panel border-r border-tsl18-line -translate-x-full transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<h2 class="px-2.5 pb-3.5 text-xs font-semibold uppercase tracking-[0.08em] text-tsl18-mut">Layers</h2>
<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-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover aria-[current=page]:text-tsl18-accent aria-[current=page]:bg-tsl18-tint focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Base map</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Traffic density</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Transit lines</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Bike network</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">3D buildings</a>
</nav>
</aside>
<div class="tsl-18-canvas h-full grid grid-rows-[auto_1fr] translate-x-0 transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl18-panel border-b border-tsl18-line text-[15px] font-semibold">
<label for="tsl18-p" class="inline-flex items-center gap-2 min-h-11 px-3.5 -ml-1 rounded-[10px] border border-tsl18-line text-[13px] font-medium text-tsl18-ink cursor-pointer transition-colors duration-200 hover:bg-tsl18-hover"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-none stroke-current stroke-[1.9] [stroke-linecap:round]"><path d="M4 6h16M4 12h16M4 18h16"/></svg><span>Layers</span></label>
<strong>Metroview</strong>
</header>
<main class="grid place-items-center bg-[repeating-linear-gradient(0deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),repeating-linear-gradient(90deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),oklch(0.16_0.012_220)]">
<p class="max-w-[40ch] px-6 text-center text-sm leading-relaxed text-tsl18-mut text-pretty">Open Layers — the whole canvas slides right by exactly 280px (one shared variable), never squeezing. Both moves are GPU transforms.</p>
</main>
</div>
</section><section class="tsl-18 relative w-full h-svh min-h-svh overflow-hidden [--w:280px] font-tsl18-sans bg-tsl18-bg text-tsl18-ink">
<input type="checkbox" id="tsl18-p" class="sr-only peer">
<aside aria-label="Layers" class="tsl-18-drawer absolute inset-y-0 left-0 z-[2] w-[var(--w)] px-3.5 py-5 bg-tsl18-panel border-r border-tsl18-line -translate-x-full transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<h2 class="px-2.5 pb-3.5 text-xs font-semibold uppercase tracking-[0.08em] text-tsl18-mut">Layers</h2>
<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-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover aria-[current=page]:text-tsl18-accent aria-[current=page]:bg-tsl18-tint focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Base map</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Traffic density</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Transit lines</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Bike network</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">3D buildings</a>
</nav>
</aside>
<div class="tsl-18-canvas h-full grid grid-rows-[auto_1fr] translate-x-0 transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl18-panel border-b border-tsl18-line text-[15px] font-semibold">
<label for="tsl18-p" class="inline-flex items-center gap-2 min-h-11 px-3.5 -ml-1 rounded-[10px] border border-tsl18-line text-[13px] font-medium text-tsl18-ink cursor-pointer transition-colors duration-200 hover:bg-tsl18-hover"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-none stroke-current stroke-[1.9] [stroke-linecap:round]"><path d="M4 6h16M4 12h16M4 18h16"/></svg><span>Layers</span></label>
<strong>Metroview</strong>
</header>
<main class="grid place-items-center bg-[repeating-linear-gradient(0deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),repeating-linear-gradient(90deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),oklch(0.16_0.012_220)]">
<p class="max-w-[40ch] px-6 text-center text-sm leading-relaxed text-tsl18-mut text-pretty">Open Layers — the whole canvas slides right by exactly 280px (one shared variable), never squeezing. Both moves are GPU transforms.</p>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}@import "tailwindcss";
@theme {
--color-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}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-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}@import "tailwindcss";
@theme {
--color-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- A custom property as an arbitrary utility (
[--w:280px]) is the cleanest way to keep two elements in lockstep in Tailwind. It is also a good habit boundary: the variable is per-instance geometry, so it does NOT belong in@theme. - UI change from the CSS original: the drawer rows and the Layers toggle are
min-h-11, and the toggle's focus ring comes from a[:has(#tsl18-p:focus-visible)_&]:variant on the label — the input is the shell's first child, not the label's sibling, sopeer-focus-visible:could never have reached it.
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 Push-Content Sidebar Drawer with GPU Transitions
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-push-content-sidebar-drawer/
The drawer that pushes instead of covering: opening slides the entire canvas right by exactly the drawer's width, both elements moving as one welded unit. One shared variable feeds the drawer width and the canvas displacement, so they cannot drift.
## HTML
```html
<section class="tsl-18 relative w-full h-svh min-h-svh overflow-hidden [--w:280px] font-tsl18-sans bg-tsl18-bg text-tsl18-ink">
<input type="checkbox" id="tsl18-p" class="sr-only peer">
<aside aria-label="Layers" class="tsl-18-drawer absolute inset-y-0 left-0 z-[2] w-[var(--w)] px-3.5 py-5 bg-tsl18-panel border-r border-tsl18-line -translate-x-full transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<h2 class="px-2.5 pb-3.5 text-xs font-semibold uppercase tracking-[0.08em] text-tsl18-mut">Layers</h2>
<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-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover aria-[current=page]:text-tsl18-accent aria-[current=page]:bg-tsl18-tint focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Base map</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Traffic density</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Transit lines</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Bike network</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">3D buildings</a>
</nav>
</aside>
<div class="tsl-18-canvas h-full grid grid-rows-[auto_1fr] translate-x-0 transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl18-panel border-b border-tsl18-line text-[15px] font-semibold">
<label for="tsl18-p" class="inline-flex items-center gap-2 min-h-11 px-3.5 -ml-1 rounded-[10px] border border-tsl18-line text-[13px] font-medium text-tsl18-ink cursor-pointer transition-colors duration-200 hover:bg-tsl18-hover"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-none stroke-current stroke-[1.9] [stroke-linecap:round]"><path d="M4 6h16M4 12h16M4 18h16"/></svg><span>Layers</span></label>
<strong>Metroview</strong>
</header>
<main class="grid place-items-center bg-[repeating-linear-gradient(0deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),repeating-linear-gradient(90deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),oklch(0.16_0.012_220)]">
<p class="max-w-[40ch] px-6 text-center text-sm leading-relaxed text-tsl18-mut text-pretty">Open Layers — the whole canvas slides right by exactly 280px (one shared variable), never squeezing. Both moves are GPU transforms.</p>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}
```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 Push-Content Sidebar Drawer with GPU Transitions
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-push-content-sidebar-drawer/
The drawer that pushes instead of covering: opening slides the entire canvas right by exactly the drawer's width, both elements moving as one welded unit. One shared variable feeds the drawer width and the canvas displacement, so they cannot drift.
## HTML
```html
<section class="tsl-18 relative w-full h-svh min-h-svh overflow-hidden [--w:280px] font-tsl18-sans bg-tsl18-bg text-tsl18-ink">
<input type="checkbox" id="tsl18-p" class="sr-only peer">
<aside aria-label="Layers" class="tsl-18-drawer absolute inset-y-0 left-0 z-[2] w-[var(--w)] px-3.5 py-5 bg-tsl18-panel border-r border-tsl18-line -translate-x-full transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<h2 class="px-2.5 pb-3.5 text-xs font-semibold uppercase tracking-[0.08em] text-tsl18-mut">Layers</h2>
<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-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover aria-[current=page]:text-tsl18-accent aria-[current=page]:bg-tsl18-tint focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Base map</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Traffic density</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Transit lines</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">Bike network</a>
<a href="#" class="flex items-center min-h-11 px-3.5 rounded-[10px] text-sm font-medium text-tsl18-mut no-underline transition-colors duration-[180ms] hover:text-tsl18-ink hover:bg-tsl18-hover focus-visible:outline-2 focus-visible:outline-tsl18-accent focus-visible:outline-offset-2">3D buildings</a>
</nav>
</aside>
<div class="tsl-18-canvas h-full grid grid-rows-[auto_1fr] translate-x-0 transition-transform duration-[320ms] ease-[cubic-bezier(.4,0,.2,1)] motion-reduce:transition-none">
<header class="flex items-center gap-3.5 h-[54px] px-4 bg-tsl18-panel border-b border-tsl18-line text-[15px] font-semibold">
<label for="tsl18-p" class="inline-flex items-center gap-2 min-h-11 px-3.5 -ml-1 rounded-[10px] border border-tsl18-line text-[13px] font-medium text-tsl18-ink cursor-pointer transition-colors duration-200 hover:bg-tsl18-hover"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-none stroke-current stroke-[1.9] [stroke-linecap:round]"><path d="M4 6h16M4 12h16M4 18h16"/></svg><span>Layers</span></label>
<strong>Metroview</strong>
</header>
<main class="grid place-items-center bg-[repeating-linear-gradient(0deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),repeating-linear-gradient(90deg,transparent_0_39px,oklch(0.2_0.014_220)_39px_40px),oklch(0.16_0.012_220)]">
<p class="max-w-[40ch] px-6 text-center text-sm leading-relaxed text-tsl18-mut text-pretty">Open Layers — the whole canvas slides right by exactly 280px (one shared variable), never squeezing. Both moves are GPU transforms.</p>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl18-bg: #101619;
--color-tsl18-bg: oklch(0.14 0.012 220);
--color-tsl18-panel: #161f24;
--color-tsl18-panel: oklch(0.18 0.015 220);
--color-tsl18-ink: #e8eef1;
--color-tsl18-ink: oklch(0.94 0.005 220);
--color-tsl18-mut: #929da3;
--color-tsl18-mut: oklch(0.64 0.012 220);
--color-tsl18-line: #343f45;
--color-tsl18-line: oklch(0.29 0.016 220);
--color-tsl18-accent: #57c2e0;
--color-tsl18-accent: oklch(0.75 0.13 220);
--color-tsl18-hover: rgb(232 238 241 / 0.06);
--color-tsl18-hover: color-mix(in oklab, oklch(0.94 0.005 220) 6%, transparent);
--color-tsl18-tint: rgb(87 194 224 / 0.14);
--color-tsl18-tint: color-mix(in oklab, oklch(0.75 0.13 220) 14%, transparent);
--font-tsl18-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tsl-18 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — drawer and canvas move by the SAME variable, in the same frame. */
#tsl18-p:checked ~ .tsl-18-drawer {
translate: 0;
}
#tsl18-p:checked ~ .tsl-18-canvas {
translate: var(--w) 0;
}
#tsl18-p:focus-visible ~ .tsl-18-canvas label[for="tsl18-p"] {
outline: 2px solid var(--color-tsl18-accent);
outline-offset: 2px;
}
```How this works
Push versus squeeze is a product decision, and this demo is the push half. The canvas keeps its own width and slides off the right edge rather than reflowing — correct for editors and maps, where a mid-animation reflow would be disruptive. Demos 02 and 06 are the squeeze half, animating a grid track instead.
The variable is what makes it maintainable. [--w:280px] is an arbitrary property on the shell, and both the drawer's width and the canvas's displacement read it. Change one number and the mechanism recalibrates; there is no second place where 280 appears.
Make it yours
- Squeeze instead: swap the canvas displacement for demo 06's grid-track animation when content should reflow.
- Partial push:
has-checked:translate-x-[calc(var(--w)/2)]for the iPad split-view feel. - Inertia:
ease-[linear(0,1.05_70%,1)]gives the push a physical settle.
Gotchas — read before shipping
- Transformed ancestors capture
position:fixeddescendants — modals and toasts inside the pushed canvas will travel with it. Use top-layer popovers instead. - The canvas's trailing edge goes off-viewport. Keep critical actions on the leading half, or choose squeeze.
overflow-hiddenon the shell, or the displacement creates a horizontal scrollbar.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() gates the state wiring; the transform push itself works in every engine since 2013.