21 / 24
All 24 in 24 Tailwind Liquid Glass Navbars
- 01Tailwind iOS 26 Floating Tab Bar
- 02Tailwind macOS Tahoe Menu Bar
- 03Tailwind Safari Toolbar with SVG Refraction
- 04Tailwind visionOS Glass Navbar
- 05Tailwind Liquid Glass Bottom Accessory Bar
- 06Tailwind Dynamic Island Morphing Nav
- 07Tailwind Liquid Glass Navbar (Canonical)
- 08Tailwind Responsive Glass Navbar with Burger
- 09Tailwind Next.js Sticky Glass Header
- 10Tailwind Spring Glide Menu (Framer Motion Port)
- 11Tailwind CSS Variables Glass Theme Generator
- 12Tailwind Glass Override for Bootstrap 5 Navbars
- 13Tailwind Scroll-Opacity Sticky Navbar
- 14Tailwind Inner-Glow Glass Nav Links
- 15Tailwind Cursor-Lens Glass Nav (GSAP-ready)
- 16Tailwind Animated Gradient Blobs Glass Navbar
- 17Tailwind Sliding Capsule Tab Bar (group-has)
- 18Tailwind Reflective Glass Sidebar
- 19Tailwind Fallback-First Glass Navbar
- 20Tailwind High-Contrast Accessible Glass Navbar
- 21Tailwind Light / Dark Liquid Glass Navbar
- 22Tailwind Performance-Tuned Glass Navbar
- 23Tailwind Minimalist Dual-Border Glass Header
- 24Tailwind Responsive Glass Sidebar to Bottom Tabs
Tailwind Light / Dark Liquid Glass Navbar
A glass navbar that retunes its whole recipe — fill, rim, saturation, field colour — for light or dark, automatically from the OS and manually from a checkbox. The port makes a real architectural choice: rather than doubling every utility with a dark: twin, the bar reads four tokens and the tokens flip in two small CSS rules.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="nbtw-21 grid place-items-center w-full min-h-svh p-5 font-nb21-sans" aria-label="Light and dark mode glass navbar demo">
<div class="w-[min(720px,100%)] px-5.5 py-10 rounded-[22px] bg-[var(--nb21-stage)] transition-[background] duration-400 motion-reduce:transition-none">
<input class="peer sr-only" type="checkbox" id="nbtw-21-toggle">
<header class="flex flex-wrap items-center gap-x-4.5 gap-y-3 px-4 py-3.5 rounded-2xl text-[var(--nb21-fg)] bg-[var(--nb21-glass)] border border-[var(--nb21-rim)] shadow-[inset_0_1px_0_var(--nb21-rim),0_16px_38px_-22px_rgba(0,0,0,.5)] backdrop-blur-[20px] backdrop-saturate-(--nb21-sat) transition-[background-color,color,border-color] duration-400 peer-checked:[&_[data-knob]]:translate-x-[22px] peer-focus-visible:[&_label]:outline-2 peer-focus-visible:[&_label]:outline-current peer-focus-visible:[&_label]:outline-offset-2 motion-reduce:transition-none">
<span class="text-base font-extrabold">◑ Adaptive</span>
<nav class="flex flex-wrap gap-1 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2 aria-[current=page]:opacity-100 aria-[current=page]:bg-[var(--nb21-field)]" href="#nbtw-21" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Notes</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Files</a>
</nav>
<label class="flex items-center w-13 h-7.5 p-[3px] rounded-full cursor-pointer bg-[var(--nb21-field)] border border-[var(--nb21-rim)] transition-colors duration-300" for="nbtw-21-toggle" title="Toggle theme">
<span class="size-5.5 rounded-full bg-current transition-transform duration-300 ease-nb21 motion-reduce:transition-none" data-knob aria-hidden="true"></span>
<span class="sr-only">Toggle light / dark theme</span>
</label>
</header>
</div>
</section><section class="nbtw-21 grid place-items-center w-full min-h-svh p-5 font-nb21-sans" aria-label="Light and dark mode glass navbar demo">
<div class="w-[min(720px,100%)] px-5.5 py-10 rounded-[22px] bg-[var(--nb21-stage)] transition-[background] duration-400 motion-reduce:transition-none">
<input class="peer sr-only" type="checkbox" id="nbtw-21-toggle">
<header class="flex flex-wrap items-center gap-x-4.5 gap-y-3 px-4 py-3.5 rounded-2xl text-[var(--nb21-fg)] bg-[var(--nb21-glass)] border border-[var(--nb21-rim)] shadow-[inset_0_1px_0_var(--nb21-rim),0_16px_38px_-22px_rgba(0,0,0,.5)] backdrop-blur-[20px] backdrop-saturate-(--nb21-sat) transition-[background-color,color,border-color] duration-400 peer-checked:[&_[data-knob]]:translate-x-[22px] peer-focus-visible:[&_label]:outline-2 peer-focus-visible:[&_label]:outline-current peer-focus-visible:[&_label]:outline-offset-2 motion-reduce:transition-none">
<span class="text-base font-extrabold">◑ Adaptive</span>
<nav class="flex flex-wrap gap-1 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2 aria-[current=page]:opacity-100 aria-[current=page]:bg-[var(--nb21-field)]" href="#nbtw-21" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Notes</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Files</a>
</nav>
<label class="flex items-center w-13 h-7.5 p-[3px] rounded-full cursor-pointer bg-[var(--nb21-field)] border border-[var(--nb21-rim)] transition-colors duration-300" for="nbtw-21-toggle" title="Toggle theme">
<span class="size-5.5 rounded-full bg-current transition-transform duration-300 ease-nb21 motion-reduce:transition-none" data-knob aria-hidden="true"></span>
<span class="sr-only">Toggle light / dark theme</span>
</label>
</header>
</div>
</section>@import "tailwindcss";
@theme {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}@import "tailwindcss";
@theme {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}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 {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}@import "tailwindcss";
@theme {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The architectural call: read tokens rather than doubling every utility with a
dark:twin. One design, two value sets, and a manual override that has something to hook into. - The token flips are the deliberate exception to keeping everything in the markup — a theme is exactly the kind of thing that belongs in one place.
- Saturation is a token because the correct value genuinely differs between light and dark glass; a fixed
backdrop-saturate-*looks wrong in one of the two. backdrop-saturate-(--nb21-sat)reads a unitless multiplier, which is why the token is1.7and not170%.- The manual override is
:has(#toggle:checked)on the root, ordered after the media query so a user's explicit choice beats the OS. - The switch is
peermechanics with the sibling caveat handled: both variants sit on the header (the checkbox's actual sibling) and reach the knob and the track with child selectors. Put them on the knob and they compile, never match, and leave a dead-looking control on a working theme.
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 Light / Dark Liquid Glass Navbar
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-light-dark-liquid-glass-navbar/
A glass navbar that retunes its whole recipe — fill, rim, saturation, field colour — for light or dark, automatically from the OS and manually from a checkbox. The port makes a real architectural choice: rather than doubling every utility with a dark: twin, the bar reads four tokens and the tokens flip in two small CSS rules.
## HTML
```html
<section class="nbtw-21 grid place-items-center w-full min-h-svh p-5 font-nb21-sans" aria-label="Light and dark mode glass navbar demo">
<div class="w-[min(720px,100%)] px-5.5 py-10 rounded-[22px] bg-[var(--nb21-stage)] transition-[background] duration-400 motion-reduce:transition-none">
<input class="peer sr-only" type="checkbox" id="nbtw-21-toggle">
<header class="flex flex-wrap items-center gap-x-4.5 gap-y-3 px-4 py-3.5 rounded-2xl text-[var(--nb21-fg)] bg-[var(--nb21-glass)] border border-[var(--nb21-rim)] shadow-[inset_0_1px_0_var(--nb21-rim),0_16px_38px_-22px_rgba(0,0,0,.5)] backdrop-blur-[20px] backdrop-saturate-(--nb21-sat) transition-[background-color,color,border-color] duration-400 peer-checked:[&_[data-knob]]:translate-x-[22px] peer-focus-visible:[&_label]:outline-2 peer-focus-visible:[&_label]:outline-current peer-focus-visible:[&_label]:outline-offset-2 motion-reduce:transition-none">
<span class="text-base font-extrabold">◑ Adaptive</span>
<nav class="flex flex-wrap gap-1 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2 aria-[current=page]:opacity-100 aria-[current=page]:bg-[var(--nb21-field)]" href="#nbtw-21" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Notes</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Files</a>
</nav>
<label class="flex items-center w-13 h-7.5 p-[3px] rounded-full cursor-pointer bg-[var(--nb21-field)] border border-[var(--nb21-rim)] transition-colors duration-300" for="nbtw-21-toggle" title="Toggle theme">
<span class="size-5.5 rounded-full bg-current transition-transform duration-300 ease-nb21 motion-reduce:transition-none" data-knob aria-hidden="true"></span>
<span class="sr-only">Toggle light / dark theme</span>
</label>
</header>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
```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 Light / Dark Liquid Glass Navbar
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-light-dark-liquid-glass-navbar/
A glass navbar that retunes its whole recipe — fill, rim, saturation, field colour — for light or dark, automatically from the OS and manually from a checkbox. The port makes a real architectural choice: rather than doubling every utility with a dark: twin, the bar reads four tokens and the tokens flip in two small CSS rules.
## HTML
```html
<section class="nbtw-21 grid place-items-center w-full min-h-svh p-5 font-nb21-sans" aria-label="Light and dark mode glass navbar demo">
<div class="w-[min(720px,100%)] px-5.5 py-10 rounded-[22px] bg-[var(--nb21-stage)] transition-[background] duration-400 motion-reduce:transition-none">
<input class="peer sr-only" type="checkbox" id="nbtw-21-toggle">
<header class="flex flex-wrap items-center gap-x-4.5 gap-y-3 px-4 py-3.5 rounded-2xl text-[var(--nb21-fg)] bg-[var(--nb21-glass)] border border-[var(--nb21-rim)] shadow-[inset_0_1px_0_var(--nb21-rim),0_16px_38px_-22px_rgba(0,0,0,.5)] backdrop-blur-[20px] backdrop-saturate-(--nb21-sat) transition-[background-color,color,border-color] duration-400 peer-checked:[&_[data-knob]]:translate-x-[22px] peer-focus-visible:[&_label]:outline-2 peer-focus-visible:[&_label]:outline-current peer-focus-visible:[&_label]:outline-offset-2 motion-reduce:transition-none">
<span class="text-base font-extrabold">◑ Adaptive</span>
<nav class="flex flex-wrap gap-1 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2 aria-[current=page]:opacity-100 aria-[current=page]:bg-[var(--nb21-field)]" href="#nbtw-21" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Notes</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-current opacity-80 text-[14.5px] font-semibold transition-[background-color,opacity] duration-200 hover:opacity-100 hover:bg-[var(--nb21-field)] focus-visible:outline-2 focus-visible:outline-current focus-visible:outline-offset-2" href="#nbtw-21">Files</a>
</nav>
<label class="flex items-center w-13 h-7.5 p-[3px] rounded-full cursor-pointer bg-[var(--nb21-field)] border border-[var(--nb21-rim)] transition-colors duration-300" for="nbtw-21-toggle" title="Toggle theme">
<span class="size-5.5 rounded-full bg-current transition-transform duration-300 ease-nb21 motion-reduce:transition-none" data-knob aria-hidden="true"></span>
<span class="sr-only">Toggle light / dark theme</span>
</label>
</header>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--font-nb21-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb21: cubic-bezier(0.34, 1.4, 0.5, 1);
}
/* The light token set, and the reason this demo reads tokens instead of
doubling every utility with a dark: twin. */
.nbtw-21 {
--nb21-glass: rgba(255, 255, 255, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.8);
--nb21-fg: #12131a;
--nb21-field: rgba(0, 0, 0, 0.08);
--nb21-sat: 1.7;
--nb21-stage: linear-gradient(120deg, #dfe9f3, #ffffff);
}
/* Automatic: the OS setting repoints the same five tokens. */
@media (prefers-color-scheme: dark) {
.nbtw-21 {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
}
/* Manual override, last so it wins over the media query. A checkbox read with
:has() is the whole mechanism — no JavaScript, no flash. */
.nbtw-21:has(#nbtw-21-toggle:checked) {
--nb21-glass: rgba(20, 22, 34, 0.5);
--nb21-rim: rgba(255, 255, 255, 0.4);
--nb21-fg: #f2f3fb;
--nb21-field: rgba(255, 255, 255, 0.1);
--nb21-sat: 1.5;
--nb21-stage: linear-gradient(120deg, #12141f, #242a44);
}
```How this works
The choice is tokens, not dark: twins. A dark: variant on every utility would double the class list and leave the manual override with nothing to hook into. Reading tokens instead means one design and two value sets:
class="bg-[var(--nb21-glass)] border-[var(--nb21-rim)]
text-[var(--nb21-fg)] backdrop-saturate-(--nb21-sat)"@media (prefers-color-scheme: dark) { .nbtw-21 { --nb21-glass: rgba(20,22,34,.5); … } }
.nbtw-21:has(#nbtw-21-toggle:checked) { --nb21-glass: rgba(20,22,34,.5); … }The glass recipe adapts, not just the colours. Light glass wants a brighter fill and more saturation; dark glass wants a translucent slate and less, or the colours behind it turn lurid. That is why saturation is a token here rather than a fixed utility.
The override is :has(), and it is pure CSS. A native checkbox holds the manual choice, :has() reads it on the root, and the token block flips regardless of the OS setting. No JavaScript, no flash, and the control stays keyboard-operable because it is sr-only rather than hidden.
The switch is peer mechanics, with the trap that catches everyone. peer-* compiles to a sibling selector, and the checkbox's only sibling here is the header — not the knob two levels down. So the variant goes on the header and reaches in with a child selector:
<input class="peer sr-only" …>
<header class="peer-checked:[&_[data-knob]]:translate-x-[22px]
peer-focus-visible:[&_label]:outline-2">Written on the knob itself, peer-checked:translate-x-[22px] compiles cleanly and silently never matches — the theme still flips through :has() while the control sits dead.
Make it yours
- Add themed tokens (shadow, accent, field) rather than more variants; every utility that should adapt reads one of them.
- Invert the priority by making the checkbox mean "force light" instead, or add a third state with a radio group and one more token block.
- Persist the manual choice with a few lines of JavaScript and
localStorage; the CSS does not change. - Use
dark:variants instead if only two or three utilities differ — the token approach earns its keep when many do. - Retune saturation per theme first if the glass looks wrong after a palette change; it is the token people forget.
Gotchas — read before shipping
- Redefine the same tokens per theme. Forking whole rule sets per theme is how the two versions drift apart over time.
- Verify contrast in both themes, dark glass especially — a 50% slate fill over a dark stage is the easiest place to fail.
peer-*matches previous siblings only. The knob and the label are descendants of the header, so the variants belong on the header with a child selector bolted on (peer-checked:[&_[data-knob]]:…) — on the knob they compile and never match.- Keep the checkbox
sr-only, nothidden, or the manual override is unreachable by keyboard. - In v4
dark:follows the OS by default. If you want a class-based strategy you must configure it — and if you also ship a:has()override, decide which wins. backdrop-saturate-(--nb21-sat)needs a unitless multiplier in the token (1.7), not a percentage.- The
:has()rule and the media query can disagree. Ordering the:has()rule last is what makes the manual choice authoritative.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4 sets the floor and :has() is inside it. prefers-color-scheme is universal; drop the :has() override for an auto-only version that works even further back.