01 / 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 SaaS Dashboard Sidebar with Native Dark Mode Switch
The 2027 SaaS shell in one file: brand, nav, user badge and a real light/dark switch that re-skins the ENTIRE app from a single checkbox. The port's win is that the theme flip gets simpler in v4 — every utility already compiles to var(--color-tsl01-*), so one scoped rule re-points the tokens and the whole shell follows without a single conditional class.
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-01 w-full h-svh min-h-svh grid grid-cols-[250px_1fr] font-tsl01-sans bg-tsl01-bg text-tsl01-ink transition-colors duration-[350ms]">
<aside class="flex flex-col gap-1.5 px-3.5 py-5 bg-tsl01-panel border-r border-tsl01-line transition-colors duration-[350ms]">
<div class="flex items-center gap-2.5 px-2.5 pt-1 pb-[18px] text-base font-bold tracking-tight">
<span aria-hidden="true" class="w-[26px] h-[26px] rounded-lg bg-[conic-gradient(from_210deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]"></span>Nimbus
</div>
<nav aria-label="Main" class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover aria-[current=page]:text-tsl01-accentink aria-[current=page]:bg-tsl01-active focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg>Overview</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg>Analytics</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5m1-13.5a3.5 3.5 0 0 1 0 7"/></svg>Customers</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>Billing</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M4.9 4.9l2.1 2.1m10 10 2.1 2.1m0-14.2-2.1 2.1m-10 10-2.1 2.1"/></svg>Settings</a>
</nav>
<label class="tsl-01-switch mt-auto flex items-center gap-2.5 min-h-11 px-3 rounded-[10px] text-[13px] font-medium text-tsl01-mut cursor-pointer transition-colors hover:text-tsl01-ink">
<input type="checkbox" id="tsl01-sw" class="sr-only">
<span aria-hidden="true" class="tsl-01-track w-9 h-5 shrink-0 rounded-full bg-tsl01-line p-0.5 transition-colors duration-[250ms]">
<span class="tsl-01-thumb block w-4 h-4 rounded-full bg-tsl01-ink transition-[translate,background-color] duration-[250ms] motion-reduce:transition-colors"></span>
</span>Light mode
</label>
<div class="flex items-center gap-2.5 px-2.5 py-3 border-t border-tsl01-line text-[13px] leading-[1.35] font-medium transition-colors duration-[350ms]">
<span aria-hidden="true" class="grid place-items-center w-8 h-8 shrink-0 rounded-full text-[11px] font-bold text-white bg-[linear-gradient(135deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]">AK</span>
<span><strong class="block">Ada Kim</strong><small class="block text-[11.5px] font-normal text-tsl01-mut">ada@nimbus.io</small></span>
</div>
</aside>
<main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-5">Good morning, Ada</h1>
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-3.5">
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">MRR</small><strong class="text-[22px] font-semibold tabular-nums">$48,210</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Active users</small><strong class="text-[22px] font-semibold tabular-nums">12,904</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Churn</small><strong class="text-[22px] font-semibold tabular-nums">1.8%</strong></article>
</div>
</main>
</section><section class="tsl-01 w-full h-svh min-h-svh grid grid-cols-[250px_1fr] font-tsl01-sans bg-tsl01-bg text-tsl01-ink transition-colors duration-[350ms]">
<aside class="flex flex-col gap-1.5 px-3.5 py-5 bg-tsl01-panel border-r border-tsl01-line transition-colors duration-[350ms]">
<div class="flex items-center gap-2.5 px-2.5 pt-1 pb-[18px] text-base font-bold tracking-tight">
<span aria-hidden="true" class="w-[26px] h-[26px] rounded-lg bg-[conic-gradient(from_210deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]"></span>Nimbus
</div>
<nav aria-label="Main" class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover aria-[current=page]:text-tsl01-accentink aria-[current=page]:bg-tsl01-active focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg>Overview</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg>Analytics</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5m1-13.5a3.5 3.5 0 0 1 0 7"/></svg>Customers</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>Billing</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M4.9 4.9l2.1 2.1m10 10 2.1 2.1m0-14.2-2.1 2.1m-10 10-2.1 2.1"/></svg>Settings</a>
</nav>
<label class="tsl-01-switch mt-auto flex items-center gap-2.5 min-h-11 px-3 rounded-[10px] text-[13px] font-medium text-tsl01-mut cursor-pointer transition-colors hover:text-tsl01-ink">
<input type="checkbox" id="tsl01-sw" class="sr-only">
<span aria-hidden="true" class="tsl-01-track w-9 h-5 shrink-0 rounded-full bg-tsl01-line p-0.5 transition-colors duration-[250ms]">
<span class="tsl-01-thumb block w-4 h-4 rounded-full bg-tsl01-ink transition-[translate,background-color] duration-[250ms] motion-reduce:transition-colors"></span>
</span>Light mode
</label>
<div class="flex items-center gap-2.5 px-2.5 py-3 border-t border-tsl01-line text-[13px] leading-[1.35] font-medium transition-colors duration-[350ms]">
<span aria-hidden="true" class="grid place-items-center w-8 h-8 shrink-0 rounded-full text-[11px] font-bold text-white bg-[linear-gradient(135deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]">AK</span>
<span><strong class="block">Ada Kim</strong><small class="block text-[11.5px] font-normal text-tsl01-mut">ada@nimbus.io</small></span>
</div>
</aside>
<main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-5">Good morning, Ada</h1>
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-3.5">
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">MRR</small><strong class="text-[22px] font-semibold tabular-nums">$48,210</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Active users</small><strong class="text-[22px] font-semibold tabular-nums">12,904</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Churn</small><strong class="text-[22px] font-semibold tabular-nums">1.8%</strong></article>
</div>
</main>
</section>@import "tailwindcss";
@theme {
--color-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}@import "tailwindcss";
@theme {
--color-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}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-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}@import "tailwindcss";
@theme {
--color-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}<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.
- The theme flip is the demo, and it ports almost unchanged — but for a better reason than in the CSS original.
@thememakes the tokens utilities, so the markup never names a colour twice; the override rule then re-points all of them at once. Adark:-variant port of the same shell would have added roughly forty extra classes. peer-*compiles to a general-SIBLING selector, so it reaches the track but never the thumb nested inside it — the trap this port hit head-on.[:has(#tsl01-sw:checked)_&]:— an id-scoped ancestor variant — reads the checkbox from any depth and carries enough specificity to beat the resting utility, which the zero-specificity:where()wrapper behindgroup-has-*does not reliably do.- UI change from the CSS original: nav rows went from 40px to
min-h-11(44px) to meet WCAG 2.5.8 target size, metric values gainedtabular-numsso live numbers stop jittering, and the main pane gainedscrollbar-gutter:stableso content does not shift when the scrollbar appears.
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 SaaS Dashboard Sidebar with Native Dark Mode Switch
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-saas-dashboard-sidebar-dark-mode-switch/
The 2027 SaaS shell in one file: brand, nav, user badge and a real light/dark switch that re-skins the ENTIRE app from a single checkbox. The port's win is that the theme flip gets simpler in v4 — every utility already compiles to var(--color-tsl01-*), so one scoped rule re-points the tokens and the whole shell follows without a single conditional class.
## HTML
```html
<section class="tsl-01 w-full h-svh min-h-svh grid grid-cols-[250px_1fr] font-tsl01-sans bg-tsl01-bg text-tsl01-ink transition-colors duration-[350ms]">
<aside class="flex flex-col gap-1.5 px-3.5 py-5 bg-tsl01-panel border-r border-tsl01-line transition-colors duration-[350ms]">
<div class="flex items-center gap-2.5 px-2.5 pt-1 pb-[18px] text-base font-bold tracking-tight">
<span aria-hidden="true" class="w-[26px] h-[26px] rounded-lg bg-[conic-gradient(from_210deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]"></span>Nimbus
</div>
<nav aria-label="Main" class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover aria-[current=page]:text-tsl01-accentink aria-[current=page]:bg-tsl01-active focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg>Overview</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg>Analytics</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5m1-13.5a3.5 3.5 0 0 1 0 7"/></svg>Customers</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>Billing</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M4.9 4.9l2.1 2.1m10 10 2.1 2.1m0-14.2-2.1 2.1m-10 10-2.1 2.1"/></svg>Settings</a>
</nav>
<label class="tsl-01-switch mt-auto flex items-center gap-2.5 min-h-11 px-3 rounded-[10px] text-[13px] font-medium text-tsl01-mut cursor-pointer transition-colors hover:text-tsl01-ink">
<input type="checkbox" id="tsl01-sw" class="sr-only">
<span aria-hidden="true" class="tsl-01-track w-9 h-5 shrink-0 rounded-full bg-tsl01-line p-0.5 transition-colors duration-[250ms]">
<span class="tsl-01-thumb block w-4 h-4 rounded-full bg-tsl01-ink transition-[translate,background-color] duration-[250ms] motion-reduce:transition-colors"></span>
</span>Light mode
</label>
<div class="flex items-center gap-2.5 px-2.5 py-3 border-t border-tsl01-line text-[13px] leading-[1.35] font-medium transition-colors duration-[350ms]">
<span aria-hidden="true" class="grid place-items-center w-8 h-8 shrink-0 rounded-full text-[11px] font-bold text-white bg-[linear-gradient(135deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]">AK</span>
<span><strong class="block">Ada Kim</strong><small class="block text-[11.5px] font-normal text-tsl01-mut">ada@nimbus.io</small></span>
</div>
</aside>
<main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-5">Good morning, Ada</h1>
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-3.5">
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">MRR</small><strong class="text-[22px] font-semibold tabular-nums">$48,210</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Active users</small><strong class="text-[22px] font-semibold tabular-nums">12,904</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Churn</small><strong class="text-[22px] font-semibold tabular-nums">1.8%</strong></article>
</div>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}
```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 SaaS Dashboard Sidebar with Native Dark Mode Switch
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-saas-dashboard-sidebar-dark-mode-switch/
The 2027 SaaS shell in one file: brand, nav, user badge and a real light/dark switch that re-skins the ENTIRE app from a single checkbox. The port's win is that the theme flip gets simpler in v4 — every utility already compiles to var(--color-tsl01-*), so one scoped rule re-points the tokens and the whole shell follows without a single conditional class.
## HTML
```html
<section class="tsl-01 w-full h-svh min-h-svh grid grid-cols-[250px_1fr] font-tsl01-sans bg-tsl01-bg text-tsl01-ink transition-colors duration-[350ms]">
<aside class="flex flex-col gap-1.5 px-3.5 py-5 bg-tsl01-panel border-r border-tsl01-line transition-colors duration-[350ms]">
<div class="flex items-center gap-2.5 px-2.5 pt-1 pb-[18px] text-base font-bold tracking-tight">
<span aria-hidden="true" class="w-[26px] h-[26px] rounded-lg bg-[conic-gradient(from_210deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]"></span>Nimbus
</div>
<nav aria-label="Main" class="flex flex-col gap-0.5">
<a href="#" aria-current="page" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover aria-[current=page]:text-tsl01-accentink aria-[current=page]:bg-tsl01-active focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg>Overview</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg>Analytics</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="9" cy="8" r="3.5"/><path d="M3 20c0-3.3 2.7-5.5 6-5.5s6 2.2 6 5.5m1-13.5a3.5 3.5 0 0 1 0 7"/></svg>Customers</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg>Billing</a>
<a href="#" class="flex items-center gap-3 min-h-11 px-3 rounded-[10px] text-sm font-medium text-tsl01-mut no-underline transition-colors duration-200 hover:text-tsl01-ink hover:bg-tsl01-hover focus-visible:outline-2 focus-visible:outline-tsl01-accent focus-visible:outline-offset-2"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-[18px] h-[18px] fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><circle cx="12" cy="12" r="3"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M4.9 4.9l2.1 2.1m10 10 2.1 2.1m0-14.2-2.1 2.1m-10 10-2.1 2.1"/></svg>Settings</a>
</nav>
<label class="tsl-01-switch mt-auto flex items-center gap-2.5 min-h-11 px-3 rounded-[10px] text-[13px] font-medium text-tsl01-mut cursor-pointer transition-colors hover:text-tsl01-ink">
<input type="checkbox" id="tsl01-sw" class="sr-only">
<span aria-hidden="true" class="tsl-01-track w-9 h-5 shrink-0 rounded-full bg-tsl01-line p-0.5 transition-colors duration-[250ms]">
<span class="tsl-01-thumb block w-4 h-4 rounded-full bg-tsl01-ink transition-[translate,background-color] duration-[250ms] motion-reduce:transition-colors"></span>
</span>Light mode
</label>
<div class="flex items-center gap-2.5 px-2.5 py-3 border-t border-tsl01-line text-[13px] leading-[1.35] font-medium transition-colors duration-[350ms]">
<span aria-hidden="true" class="grid place-items-center w-8 h-8 shrink-0 rounded-full text-[11px] font-bold text-white bg-[linear-gradient(135deg,var(--color-tsl01-accent),oklch(0.6_0.18_220))]">AK</span>
<span><strong class="block">Ada Kim</strong><small class="block text-[11.5px] font-normal text-tsl01-mut">ada@nimbus.io</small></span>
</div>
</aside>
<main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
<h1 class="text-[22px] font-semibold leading-tight mb-5">Good morning, Ada</h1>
<div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-3.5">
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">MRR</small><strong class="text-[22px] font-semibold tabular-nums">$48,210</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Active users</small><strong class="text-[22px] font-semibold tabular-nums">12,904</strong></article>
<article class="grid gap-2 p-[18px] rounded-2xl bg-tsl01-panel border border-tsl01-line transition-colors duration-[350ms]"><small class="text-[11px] font-medium uppercase tracking-[0.07em] text-tsl01-mut">Churn</small><strong class="text-[22px] font-semibold tabular-nums">1.8%</strong></article>
</div>
</main>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsl01-bg: #131519;
--color-tsl01-bg: oklch(0.14 0.015 265);
--color-tsl01-panel: #1a1d22;
--color-tsl01-panel: oklch(0.17 0.018 265);
--color-tsl01-ink: #e9ebee;
--color-tsl01-ink: oklch(0.93 0.005 265);
--color-tsl01-mut: #969aa2;
--color-tsl01-mut: oklch(0.65 0.01 265);
--color-tsl01-line: #3a3e45;
--color-tsl01-line: oklch(0.28 0.015 265);
--color-tsl01-accent: #4fd6a8;
--color-tsl01-accent: oklch(0.7 0.15 165);
/* Text-weight accent: the pill's label needs 4.5:1 against the tint in BOTH
skins, which the display accent only clears on the dark ground. */
--color-tsl01-accentink: #4fd6a8;
--color-tsl01-accentink: oklch(0.7 0.15 165);
--color-tsl01-hover: rgb(233 235 238 / 0.06);
--color-tsl01-hover: color-mix(in oklab, oklch(0.93 0.005 265) 6%, transparent);
--color-tsl01-active: rgb(79 214 168 / 0.16);
--color-tsl01-active: color-mix(in oklab, oklch(0.7 0.15 165) 16%, transparent);
--font-tsl01-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
utilities, so the shell is sized before the browser build has compiled. */
.tsl-01 {
width: 100%;
height: 100svh;
min-height: 100svh;
color-scheme: dark;
}
/* STATE — the switch input is the shell's descendant, not the track's sibling,
so the resting look is utilities and the checked look is three scoped rules. */
#tsl01-sw:checked ~ .tsl-01-track {
background: var(--color-tsl01-accent);
}
#tsl01-sw:checked ~ .tsl-01-track .tsl-01-thumb {
translate: 16px 0;
background: #fff;
}
#tsl01-sw:focus-visible ~ .tsl-01-track {
outline: 2px solid var(--color-tsl01-accent);
outline-offset: 2px;
}
/* THE LIGHT SKIN — the one thing that cannot be a utility, and should not be:
re-pointing the tokens re-skins every utility that reads them, with no dark:
duplicate anywhere in the markup. */
.tsl-01:has(.tsl-01-switch input:checked) {
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-panel: oklch(1 0 0);
--color-tsl01-ink: oklch(0.25 0.02 265);
--color-tsl01-mut: oklch(0.52 0.015 265);
--color-tsl01-line: oklch(0.9 0.006 265);
--color-tsl01-hover: color-mix(in oklab, oklch(0.25 0.02 265) 6%, transparent);
--color-tsl01-accentink: oklch(0.48 0.15 165);
color-scheme: light;
}
@media (prefers-reduced-motion: reduce) {
.tsl-01,
.tsl-01 * {
transition-duration: 1ms !important;
}
}
```How this works
Tokens are the theme. The @theme block declares the dark palette and mints a utility per colour. The light skin is one rule that cannot be a utility and should not try to be:
.tsl-01:has(.tsl-01-switch input:checked){
--color-tsl01-bg: oklch(0.97 0.004 100);
--color-tsl01-ink: oklch(0.25 0.02 265);
color-scheme: light;
}Every bg-tsl01-bg in the markup resolves through that variable, so the flip cascades to sidebar, cards and text with zero class churn. This is why dark: variants are the wrong tool for an in-app theme switch: they double the class list on every element, while token re-pointing touches one selector.
The switch is a real checkbox. It sits sr-only inside the label; the track reads [:has(#tsl01-sw:checked)_&]:bg-tsl01-accent and the thumb [:has(#tsl01-sw:checked)_&]:translate-x-4. Keyboard toggling, screen-reader announcement and form semantics come free, and [:has(#tsl01-sw:focus-visible)_&]:outline-2 gives the ring the invisible input can't show itself.
Make it yours
- More themes: extra radios plus one
:has(#tsl01-ocean:checked)rule each — the token architecture scales to any number of palettes. - Brand accent is one token:
--color-tsl01-accentrecolours the active pill, avatar ring and switch in a single edit. - Respect the OS first by wrapping the light values in
@media (prefers-color-scheme: light)as the pre-interaction default.
Gotchas — read before shipping
- Overriding an
@themevariable in a normal rule works because@themeemits plain custom properties on:root— but the override must be on an ANCESTOR of everything that reads it. Scope it to the shell root, not to the sidebar. transition-colors, nevertransition-all:allanimates grid tracks during window resize and reads as broken.- An accent that clears 4.5:1 on a dark ground rarely clears it on a light one. Keep a separate text-weight token (
--color-tsl01-accentink) and re-point it in the light skin — the display accent can stay bright for the logo and switch. - Set
color-schemein both states, or native scrollbars and form controls stay stark white in the dark skin. sr-onlyis the right way to hide the checkbox —hiddenremoves it from the tab order and kills keyboard toggling.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() gates the theme flip (Firefox 121). The quoted floor is Tailwind v4's own generated sheet — cascade layers, @property, color-mix().