09 / 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 Next.js Sticky Glass Header
The SSR-safe header: it ships its final glass state in the server-rendered HTML, then deepens the frost past the hero. The port is the strongest argument for utilities in a Next.js app — because the class strings are static and complete, the first painted frame already matches the hydrated one, and the scroll enhancement is one data-scrolled attribute.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="nbtw-09 relative block w-full min-h-svh overflow-hidden font-nb09-sans bg-[#0b0b10]" aria-label="Next.js sticky glass header demo">
<div class="absolute inset-0 overflow-y-auto" id="nbtw-09-scroll">
<header class="sticky top-0 z-[6] flex flex-wrap items-center gap-x-4.5 gap-y-3 px-5 py-3.5 text-white bg-[rgba(20,20,28,.35)] border-b border-white/[0.08] backdrop-blur-[10px] backdrop-saturate-[1.4] shadow-none transition-[background-color,backdrop-filter,box-shadow,border-color] duration-300 data-[scrolled=true]:bg-[rgba(14,14,20,.72)] data-[scrolled=true]:border-white/[0.14] data-[scrolled=true]:backdrop-blur-[22px] data-[scrolled=true]:backdrop-saturate-[1.7] data-[scrolled=true]:shadow-[0_14px_34px_-20px_rgba(0,0,0,.8)] motion-reduce:transition-none" id="nbtw-09-bar" data-scrolled="false">
<a class="text-base font-extrabold no-underline text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">▲ next-glass</a>
<nav class="flex gap-0.5 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-white/10 aria-[current=page]:text-white" href="#nbtw-09" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Showcase</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Docs</a>
</nav>
<button class="min-h-10 px-4.5 rounded-[10px] border-0 bg-white text-[#0b0b10] text-sm font-bold cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" type="button">Deploy</button>
</header>
<div class="h-75 grid place-items-center bg-[radial-gradient(circle_at_50%_30%,#2a2350,#0b0b10_70%)]">
<h2 class="px-5 text-center text-white text-2xl font-extrabold">Scroll — the header deepens</h2>
</div>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Server-rendered glass state means no flash of an unstyled transparent bar on hydration. The class strings are static literals, so the first frame the browser paints is the finished header.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Past the hero the frost, tint, border and shadow intensify — four <code>data-[scrolled=true]:</code> utilities on the same element, not a second rule that has to out-specify the first.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">In Next.js the header is a Client Component wrapping only the scroll flag. Because the flag is an attribute, that component holds no visual knowledge at all.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]"><code>sticky top-0</code> keeps CLS at 0 — the header reserves its box before scroll begins, so nothing shifts when the tint deepens past 120px.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">The handler is <code>requestAnimationFrame</code>-throttled so the attribute write stays inside a frame — INP-friendly even on mid-tier Android.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Scroll back up: below 120px the attribute flips to false and the header returns to its light glass state through the same four transitions.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Nothing in the transition list is a layout property, which is what keeps the deepening free of reflow.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">That is the whole recipe: one attribute, two tiers, four transitions, zero flash on hydration.</p>
</div>
</section><section class="nbtw-09 relative block w-full min-h-svh overflow-hidden font-nb09-sans bg-[#0b0b10]" aria-label="Next.js sticky glass header demo">
<div class="absolute inset-0 overflow-y-auto" id="nbtw-09-scroll">
<header class="sticky top-0 z-[6] flex flex-wrap items-center gap-x-4.5 gap-y-3 px-5 py-3.5 text-white bg-[rgba(20,20,28,.35)] border-b border-white/[0.08] backdrop-blur-[10px] backdrop-saturate-[1.4] shadow-none transition-[background-color,backdrop-filter,box-shadow,border-color] duration-300 data-[scrolled=true]:bg-[rgba(14,14,20,.72)] data-[scrolled=true]:border-white/[0.14] data-[scrolled=true]:backdrop-blur-[22px] data-[scrolled=true]:backdrop-saturate-[1.7] data-[scrolled=true]:shadow-[0_14px_34px_-20px_rgba(0,0,0,.8)] motion-reduce:transition-none" id="nbtw-09-bar" data-scrolled="false">
<a class="text-base font-extrabold no-underline text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">▲ next-glass</a>
<nav class="flex gap-0.5 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-white/10 aria-[current=page]:text-white" href="#nbtw-09" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Showcase</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Docs</a>
</nav>
<button class="min-h-10 px-4.5 rounded-[10px] border-0 bg-white text-[#0b0b10] text-sm font-bold cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" type="button">Deploy</button>
</header>
<div class="h-75 grid place-items-center bg-[radial-gradient(circle_at_50%_30%,#2a2350,#0b0b10_70%)]">
<h2 class="px-5 text-center text-white text-2xl font-extrabold">Scroll — the header deepens</h2>
</div>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Server-rendered glass state means no flash of an unstyled transparent bar on hydration. The class strings are static literals, so the first frame the browser paints is the finished header.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Past the hero the frost, tint, border and shadow intensify — four <code>data-[scrolled=true]:</code> utilities on the same element, not a second rule that has to out-specify the first.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">In Next.js the header is a Client Component wrapping only the scroll flag. Because the flag is an attribute, that component holds no visual knowledge at all.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]"><code>sticky top-0</code> keeps CLS at 0 — the header reserves its box before scroll begins, so nothing shifts when the tint deepens past 120px.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">The handler is <code>requestAnimationFrame</code>-throttled so the attribute write stays inside a frame — INP-friendly even on mid-tier Android.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Scroll back up: below 120px the attribute flips to false and the header returns to its light glass state through the same four transitions.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Nothing in the transition list is a layout property, which is what keeps the deepening free of reflow.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">That is the whole recipe: one attribute, two tiers, four transitions, zero flash on hydration.</p>
</div>
</section>@import "tailwindcss";
@theme {
--font-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}@import "tailwindcss";
@theme {
--font-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}(() => {
const scroll = document.querySelector('#nbtw-09-scroll');
const bar = document.querySelector('#nbtw-09-bar');
if (!scroll || !bar) return;
let ticking = false;
scroll.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
bar.dataset.scrolled = String(scroll.scrollTop > 120);
ticking = false;
});
}, { passive: true });
})();(() => {
const scroll = document.querySelector('#nbtw-09-scroll');
const bar = document.querySelector('#nbtw-09-bar');
if (!scroll || !bar) return;
let ticking = false;
scroll.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
bar.dataset.scrolled = String(scroll.scrollTop > 120);
ticking = false;
});
}, { passive: true });
})();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-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}@import "tailwindcss";
@theme {
--font-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Static class strings are the anti-FOUC mechanism. The server emits the finished header because there is nothing to compute at runtime — which is why this demo is the strongest case for utilities in an SSR app.
- Both tiers live on one element as variants, so the deep state never has to win a specificity fight with the resting state.
- The Client Component shrinks to a scroll flag. It writes
data-scrolledand knows nothing about blur radii, which is the separation the port buys. - Never interpolate a class name at runtime.
bg-white/${n}is invisible to the compiler and reintroduces exactly the flash this pattern removes. - The transition list is four visual properties and no layout properties, so deepening the glass cannot reflow the page.
stickyrather thanfixedkeeps CLS at 0 through the state change, and the attribute is server-rendered as"false"so the first client write is a no-op.
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 Next.js Sticky Glass Header
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-next-js-sticky-glass-header/
The SSR-safe header: it ships its final glass state in the server-rendered HTML, then deepens the frost past the hero. The port is the strongest argument for utilities in a Next.js app — because the class strings are static and complete, the first painted frame already matches the hydrated one, and the scroll enhancement is one data-scrolled attribute.
## HTML
```html
<section class="nbtw-09 relative block w-full min-h-svh overflow-hidden font-nb09-sans bg-[#0b0b10]" aria-label="Next.js sticky glass header demo">
<div class="absolute inset-0 overflow-y-auto" id="nbtw-09-scroll">
<header class="sticky top-0 z-[6] flex flex-wrap items-center gap-x-4.5 gap-y-3 px-5 py-3.5 text-white bg-[rgba(20,20,28,.35)] border-b border-white/[0.08] backdrop-blur-[10px] backdrop-saturate-[1.4] shadow-none transition-[background-color,backdrop-filter,box-shadow,border-color] duration-300 data-[scrolled=true]:bg-[rgba(14,14,20,.72)] data-[scrolled=true]:border-white/[0.14] data-[scrolled=true]:backdrop-blur-[22px] data-[scrolled=true]:backdrop-saturate-[1.7] data-[scrolled=true]:shadow-[0_14px_34px_-20px_rgba(0,0,0,.8)] motion-reduce:transition-none" id="nbtw-09-bar" data-scrolled="false">
<a class="text-base font-extrabold no-underline text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">▲ next-glass</a>
<nav class="flex gap-0.5 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-white/10 aria-[current=page]:text-white" href="#nbtw-09" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Showcase</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Docs</a>
</nav>
<button class="min-h-10 px-4.5 rounded-[10px] border-0 bg-white text-[#0b0b10] text-sm font-bold cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" type="button">Deploy</button>
</header>
<div class="h-75 grid place-items-center bg-[radial-gradient(circle_at_50%_30%,#2a2350,#0b0b10_70%)]">
<h2 class="px-5 text-center text-white text-2xl font-extrabold">Scroll — the header deepens</h2>
</div>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Server-rendered glass state means no flash of an unstyled transparent bar on hydration. The class strings are static literals, so the first frame the browser paints is the finished header.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Past the hero the frost, tint, border and shadow intensify — four <code>data-[scrolled=true]:</code> utilities on the same element, not a second rule that has to out-specify the first.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">In Next.js the header is a Client Component wrapping only the scroll flag. Because the flag is an attribute, that component holds no visual knowledge at all.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]"><code>sticky top-0</code> keeps CLS at 0 — the header reserves its box before scroll begins, so nothing shifts when the tint deepens past 120px.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">The handler is <code>requestAnimationFrame</code>-throttled so the attribute write stays inside a frame — INP-friendly even on mid-tier Android.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Scroll back up: below 120px the attribute flips to false and the header returns to its light glass state through the same four transitions.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Nothing in the transition list is a layout property, which is what keeps the deepening free of reflow.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">That is the whole recipe: one attribute, two tiers, four transitions, zero flash on hydration.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--font-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}
```
## JavaScript
```js
(() => {
const scroll = document.querySelector('#nbtw-09-scroll');
const bar = document.querySelector('#nbtw-09-bar');
if (!scroll || !bar) return;
let ticking = false;
scroll.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
bar.dataset.scrolled = String(scroll.scrollTop > 120);
ticking = false;
});
}, { passive: true });
})();
```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 Next.js Sticky Glass Header
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-next-js-sticky-glass-header/
The SSR-safe header: it ships its final glass state in the server-rendered HTML, then deepens the frost past the hero. The port is the strongest argument for utilities in a Next.js app — because the class strings are static and complete, the first painted frame already matches the hydrated one, and the scroll enhancement is one data-scrolled attribute.
## HTML
```html
<section class="nbtw-09 relative block w-full min-h-svh overflow-hidden font-nb09-sans bg-[#0b0b10]" aria-label="Next.js sticky glass header demo">
<div class="absolute inset-0 overflow-y-auto" id="nbtw-09-scroll">
<header class="sticky top-0 z-[6] flex flex-wrap items-center gap-x-4.5 gap-y-3 px-5 py-3.5 text-white bg-[rgba(20,20,28,.35)] border-b border-white/[0.08] backdrop-blur-[10px] backdrop-saturate-[1.4] shadow-none transition-[background-color,backdrop-filter,box-shadow,border-color] duration-300 data-[scrolled=true]:bg-[rgba(14,14,20,.72)] data-[scrolled=true]:border-white/[0.14] data-[scrolled=true]:backdrop-blur-[22px] data-[scrolled=true]:backdrop-saturate-[1.7] data-[scrolled=true]:shadow-[0_14px_34px_-20px_rgba(0,0,0,.8)] motion-reduce:transition-none" id="nbtw-09-bar" data-scrolled="false">
<a class="text-base font-extrabold no-underline text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">▲ next-glass</a>
<nav class="flex gap-0.5 ms-auto" aria-label="Primary">
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-white/10 aria-[current=page]:text-white" href="#nbtw-09" aria-current="page">Home</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Showcase</a>
<a class="flex items-center min-h-10 px-3.5 rounded-[10px] no-underline text-white/[0.78] text-[14.5px] font-semibold transition-[background-color,color] duration-200 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-09">Docs</a>
</nav>
<button class="min-h-10 px-4.5 rounded-[10px] border-0 bg-white text-[#0b0b10] text-sm font-bold cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" type="button">Deploy</button>
</header>
<div class="h-75 grid place-items-center bg-[radial-gradient(circle_at_50%_30%,#2a2350,#0b0b10_70%)]">
<h2 class="px-5 text-center text-white text-2xl font-extrabold">Scroll — the header deepens</h2>
</div>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Server-rendered glass state means no flash of an unstyled transparent bar on hydration. The class strings are static literals, so the first frame the browser paints is the finished header.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Past the hero the frost, tint, border and shadow intensify — four <code>data-[scrolled=true]:</code> utilities on the same element, not a second rule that has to out-specify the first.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">In Next.js the header is a Client Component wrapping only the scroll flag. Because the flag is an attribute, that component holds no visual knowledge at all.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]"><code>sticky top-0</code> keeps CLS at 0 — the header reserves its box before scroll begins, so nothing shifts when the tint deepens past 120px.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">The handler is <code>requestAnimationFrame</code>-throttled so the attribute write stays inside a frame — INP-friendly even on mid-tier Android.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Scroll back up: below 120px the attribute flips to false and the header returns to its light glass state through the same four transitions.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">Nothing in the transition list is a layout property, which is what keeps the deepening free of reflow.</p>
<p class="max-w-[46ch] mx-auto p-6 text-[15px] leading-[1.7] text-white/[0.82]">That is the whole recipe: one attribute, two tiers, four transitions, zero flash on hydration.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--font-nb09-sans: system-ui, 'Segoe UI', sans-serif;
}
```
## JavaScript
```js
(() => {
const scroll = document.querySelector('#nbtw-09-scroll');
const bar = document.querySelector('#nbtw-09-bar');
if (!scroll || !bar) return;
let ticking = false;
scroll.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
bar.dataset.scrolled = String(scroll.scrollTop > 120);
ticking = false;
});
}, { passive: true });
})();
```How this works
The flash problem is a string problem. A FOUC on hydration happens when the client computes classes the server did not emit. Utilities remove that by construction: the class list is a literal in your JSX, so the server sends the finished header:
<header data-scrolled="false" class="sticky top-0 bg-[rgba(20,20,28,.35)]
backdrop-blur-[10px] backdrop-saturate-[1.4]
data-[scrolled=true]:bg-[rgba(14,14,20,.72)]
data-[scrolled=true]:backdrop-blur-[22px]">Two tiers, one element, no cascade. The deep state is not a second rule that has to out-specify the first — it is a variant of the same utility, so there is no specificity question to lose.
The handler is the whole Client Component. In Next.js the markup is server-rendered and only the scroll flag is client-side. Because the flag is an attribute rather than a class or a style, the component that owns it can be three lines and hold no visual knowledge at all.
Nothing animated is a layout property. Background, backdrop-filter, box-shadow and border-colour deepen; height, padding and position do not move. sticky reserves the box before the first scroll event, so the tint change cannot shift a single pixel of content.
Make it yours
- Set the threshold to your hero height by editing the one number in the handler; the markup does not know about it.
- Bind
data-scrolledto an IntersectionObserver on the hero instead of a pixel count for a pixel-exact transition point. - Add a reveal by pairing
border-transparentat rest withdata-[scrolled=true]:border-white/[0.14], which is what this demo does. - Deepen further with a
data-[scrolled=true]:backdrop-brightness-90if your content behind is bright. - Reuse the tier pair on a footer or sidebar; because it is variant-based, it travels as a copied class list.
Gotchas — read before shipping
- Never build the class string at runtime (
className={`bg-white/${n}`}). The compiler cannot see it, and you get the unstyled flash the pattern exists to prevent. - Use a passive scroll listener and rAF-throttle the write. An unthrottled attribute write on scroll is a measurable INP cost.
sticky, notfixed. A fixed header does not reserve space, and the CLS you save on the tint change you lose on layout.- Do not transition
backdrop-filteralongside layout properties; keep the list to the four visual ones or the frame budget disappears on mobile. - The attribute must be rendered on the server too (
data-scrolled="false"), or the first client write is a state change rather than a no-op.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4 sets the floor. The scroll deepening is enhancement; the server-rendered base state is a complete glass header on its own, which is the entire point of the demo.