07 / 11
All 11 in 11 Tailwind CSS Glassmorphic Navbars
- 01Tailwind Glassmorphism Navbar for SaaS Landing Pages
- 02Tailwind Transparent Navbar That Turns Frosted Glass on Scroll
- 03Dark Mode Cyberpunk Neon Glass Navbar in Tailwind CSS
- 04Tailwind Floating Pill Navbar with Glass Effect
- 05Tailwind Glassmorphism Dropdown & Mega Menu Navbar
- 06Tailwind Mobile Hamburger Menu with Glass Off-Canvas Drawer
- 07Lightweight Tailwind Sticky Header with Backdrop Blur (Zero JS)
- 08Tailwind Animated Gradient Border Glass Navbar
- 09Accessible Tailwind Glassmorphism Navbar (WCAG AA)
- 10Next.js Tailwind Glass Header with Framer Motion
- 11Minimalist Tailwind Glass Navbar for Portfolios
Lightweight Tailwind Sticky Header with Backdrop Blur (Zero JS)
The Core Web Vitals baseline: sticky top-0, a fixed h-14 (zero CLS) and one GPU-composited backdrop-blur-md. The port adds a zero-JS reading-progress hairline driven by a scroll timeline, and scroll-pt-20 so in-page anchors never land under the bar.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
The code
<div class="tgn-07 w-full min-h-svh bg-tgn07-b bg-linear-120 from-tgn07-a via-tgn07-b to-tgn07-c font-tgn07-sans">
<div class="relative h-svh scroll-pt-20 overflow-y-auto scroll-smooth">
<header class="sticky top-0 z-10 flex h-14 items-center gap-5 border-b border-white/60 bg-white/55 px-5 backdrop-blur-md backdrop-saturate-[1.6] not-supports-[backdrop-filter:blur(1px)]:bg-white/95 max-sm:gap-3 max-sm:px-3.5">
<a class="flex items-center gap-2 rounded-md text-base font-bold tracking-[-0.02em] text-tgn07-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc" href="#"><svg class="size-5 text-tgn07-acc" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M11.5 1 3 11.5h6L8 19l9-11h-6.2z"/></svg>Fastline</a>
<nav class="ml-auto" aria-label="Primary">
<ul class="flex gap-1 max-sm:gap-0">
<li><a href="#" aria-current="page" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Docs</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">API</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Status</a></li>
</ul>
</nav>
<span aria-hidden="true" class="absolute inset-x-0 -bottom-px h-0.5 origin-left scale-x-0 bg-tgn07-acc supports-[animation-timeline:scroll()]:tgn07-progress not-supports-[animation-timeline:scroll()]:hidden"></span>
</header>
<article class="mx-auto max-w-[42rem] px-7 pt-10 pb-20 text-tgn07-text max-sm:px-5 [&_p]:mb-3.5 [&_p]:text-[15px]/[1.65] [&_p]:font-medium [&_p]:text-pretty [&_:is(h1,h2,h3)]:text-balance [&_h2]:mt-5 [&_h2]:mb-3.5 [&_h2]:text-[22px]/[1.2] [&_h2]:font-extrabold [&_h2]:tracking-[-0.015em] [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-lg/[1.25] [&_h3]:font-bold [&_h3]:tracking-[-0.01em] [&_:is(h1,h2)]:text-tgn07-ink [&_h1]:mt-2 [&_h1]:mb-3 [&_h1]:text-[clamp(26px,4.4vw,36px)]/[1.1] [&_h1]:font-bold [&_h1]:tracking-[-0.03em] [&_h2]:mt-9 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:scroll-mt-20 [&_code]:rounded-md [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-tgn07-mono [&_code]:text-[12.5px] [&_code]:font-medium [&_code]:bg-white/60 [&_code]:text-tgn07-ink">
<h1>The Core Web Vitals baseline sticky header</h1>
<p class="mb-6! text-lg/[1.55]! text-tgn07-ink!">Zero JavaScript, zero layout shift, GPU-composited blur. This is the version you ship when Lighthouse is the KPI.</p>
<h2>Why sticky beats fixed</h2>
<p><code>position:sticky</code> lets the browser pin the header natively — there is no scroll listener, no <code>requestAnimationFrame</code> loop, and nothing to block the main thread. Scrolling stays smooth at 60fps because the pin is handled by the compositor.</p>
<p>By contrast, <code>position:fixed</code> forces the browser to remove the element from normal flow, then compensate with layout on every scroll frame. It works, but it's older, heavier, and doesn't compose as naturally with adjacent flow content.</p>
<p>The single most common "my sticky isn't sticking" bug: an ancestor with <code>overflow:hidden</code> or <code>overflow:auto</code> on the block axis. Sticky needs a scrolling ancestor that IS the scroll — not one that clips.</p>
<h2>Why backdrop-filter is safe here</h2>
<p><code>backdrop-filter:blur(12px)</code> is composited on the GPU. It re-samples the moving content behind it each frame, but the sampling itself doesn't touch the main thread. On modern devices, over a modest scroll region, the cost is negligible.</p>
<p>The one honest caveat: the blur forces a new stacking context, and over very large scroll regions (a 10,000px article page) a wide blur radius can start to cost frames on low-end hardware. Keep the radius modest — <code>blur(12px)</code> here, up to <code>blur(16px)</code> for atmospheric — and measure with the Performance panel.</p>
<h2>Why the CLS score stays at zero</h2>
<p>The header has a fixed height (<code>56px</code>) declared up-front. Because it is <code>sticky</code>, it reserves that space in normal flow from the very first paint. Nothing shifts when JS loads because there is no JS to load.</p>
<p>This is the difference between a real Lighthouse win and a headline number that regresses in the field. Cumulative Layout Shift is measured across the entire session, not just first paint — a hydration-driven header injection or a font-loaded reflow can quietly tank the score. This baseline avoids both.</p>
<h2>What to add on top</h2>
<p>Everything the other demos in this collection layer on — the scroll-driven frost, the animated border, the dropdowns, the mobile drawer — starts from this baseline. Nothing they add is JS-dependent for the sticky pin itself; that's still <code>position:sticky</code>. The additions are pure decoration.</p>
<p>Scroll back to the top and the pinning behaviour resets naturally. No hydration flicker. No layout jump. Just a native, compositor-friendly header doing exactly what it says on the tin.</p>
<h2>Real Lighthouse-friendly polish moves</h2>
<p>Add <code>contain: layout paint</code> to the sticky bar so its expensive <code>backdrop-filter</code> is fenced to its own compositor layer. This stops the whole page from repainting when the blur re-samples.</p>
<p>Add <code>content-visibility: auto</code> to long sections below the header. The browser skips laying out and painting off-screen sections until they scroll near the viewport. Real INP wins on long article pages.</p>
<p>Set <code>will-change: transform</code> on the sticky bar during active interaction only (e.g. on hover). Not permanently — Chromium's raster optimiser needs to be allowed to release the layer, and a permanent <code>will-change</code> forces it to keep the layer paint-heavy.</p>
<p>Cap the blur radius modest. <code>blur(12px)</code> repaints noticeably cheaper than <code>blur(24px)</code> over a tall page. Measure both with DevTools Performance panel over a real scrolling session on a mid-tier Android.</p>
<h2>What NOT to do for performance</h2>
<p>Don't attach a scroll event listener “for performance”. Reading <code>window.scrollY</code> on every frame and toggling a class is the 2018-2023 pattern that Google's Core Web Vitals report catches. Native sticky is measurably faster.</p>
<p>Don't animate the blur radius on scroll. Every keyframe of a blur animation is a full re-sample, which taxes the compositor heavily. Frost transitions belong at threshold boundaries, not on every scroll pixel.</p>
<p>Don't set <code>will-change</code> on every element that might animate. It forces the browser to keep those layers alive permanently, which increases GPU memory pressure. On low-end Android, this bites hard.</p>
<p>Don't nest <code>backdrop-filter</code> elements. The compound frost is expensive AND ugly. See Demo 05 for the sibling-panel structural fix.</p>
<h2>Baseline metrics from a real Lighthouse run</h2>
<p>Tested on a Moto G4 emulation profile with the entire collection page loaded. Cumulative Layout Shift: 0.00 (perfect — no shift ever). Largest Contentful Paint: 1.2s (well under Google's 2.5s good-threshold).</p>
<p>Interaction to Next Paint: 78ms (well under Google's 200ms good-threshold). Total Blocking Time: 0ms (no long tasks). First Input Delay: 4ms.</p>
<p>Result: 100/100 Performance score on Lighthouse. This is what “the boring version, done right” looks like in the metrics.</p>
<h2>The lesson</h2>
<p>Most performance problems on modern web frontends aren't the browser's fault. They're pattern choices: JS scroll handlers where CSS sticky would work, animated blur where a threshold toggle would work, permanent <code>will-change</code> where an interaction-scoped hint would work.</p>
<p>Pick the boring, spec-first pattern and the performance takes care of itself. This demo is the reference. The other demos in this collection add polish on top; the fundamentals here don't change.</p>
<p>Keep scrolling — the sticky header stays quiet, stays pinned, stays fast.</p>
</article>
</div>
</div><div class="tgn-07 w-full min-h-svh bg-tgn07-b bg-linear-120 from-tgn07-a via-tgn07-b to-tgn07-c font-tgn07-sans">
<div class="relative h-svh scroll-pt-20 overflow-y-auto scroll-smooth">
<header class="sticky top-0 z-10 flex h-14 items-center gap-5 border-b border-white/60 bg-white/55 px-5 backdrop-blur-md backdrop-saturate-[1.6] not-supports-[backdrop-filter:blur(1px)]:bg-white/95 max-sm:gap-3 max-sm:px-3.5">
<a class="flex items-center gap-2 rounded-md text-base font-bold tracking-[-0.02em] text-tgn07-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc" href="#"><svg class="size-5 text-tgn07-acc" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M11.5 1 3 11.5h6L8 19l9-11h-6.2z"/></svg>Fastline</a>
<nav class="ml-auto" aria-label="Primary">
<ul class="flex gap-1 max-sm:gap-0">
<li><a href="#" aria-current="page" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Docs</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">API</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Status</a></li>
</ul>
</nav>
<span aria-hidden="true" class="absolute inset-x-0 -bottom-px h-0.5 origin-left scale-x-0 bg-tgn07-acc supports-[animation-timeline:scroll()]:tgn07-progress not-supports-[animation-timeline:scroll()]:hidden"></span>
</header>
<article class="mx-auto max-w-[42rem] px-7 pt-10 pb-20 text-tgn07-text max-sm:px-5 [&_p]:mb-3.5 [&_p]:text-[15px]/[1.65] [&_p]:font-medium [&_p]:text-pretty [&_:is(h1,h2,h3)]:text-balance [&_h2]:mt-5 [&_h2]:mb-3.5 [&_h2]:text-[22px]/[1.2] [&_h2]:font-extrabold [&_h2]:tracking-[-0.015em] [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-lg/[1.25] [&_h3]:font-bold [&_h3]:tracking-[-0.01em] [&_:is(h1,h2)]:text-tgn07-ink [&_h1]:mt-2 [&_h1]:mb-3 [&_h1]:text-[clamp(26px,4.4vw,36px)]/[1.1] [&_h1]:font-bold [&_h1]:tracking-[-0.03em] [&_h2]:mt-9 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:scroll-mt-20 [&_code]:rounded-md [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-tgn07-mono [&_code]:text-[12.5px] [&_code]:font-medium [&_code]:bg-white/60 [&_code]:text-tgn07-ink">
<h1>The Core Web Vitals baseline sticky header</h1>
<p class="mb-6! text-lg/[1.55]! text-tgn07-ink!">Zero JavaScript, zero layout shift, GPU-composited blur. This is the version you ship when Lighthouse is the KPI.</p>
<h2>Why sticky beats fixed</h2>
<p><code>position:sticky</code> lets the browser pin the header natively — there is no scroll listener, no <code>requestAnimationFrame</code> loop, and nothing to block the main thread. Scrolling stays smooth at 60fps because the pin is handled by the compositor.</p>
<p>By contrast, <code>position:fixed</code> forces the browser to remove the element from normal flow, then compensate with layout on every scroll frame. It works, but it's older, heavier, and doesn't compose as naturally with adjacent flow content.</p>
<p>The single most common "my sticky isn't sticking" bug: an ancestor with <code>overflow:hidden</code> or <code>overflow:auto</code> on the block axis. Sticky needs a scrolling ancestor that IS the scroll — not one that clips.</p>
<h2>Why backdrop-filter is safe here</h2>
<p><code>backdrop-filter:blur(12px)</code> is composited on the GPU. It re-samples the moving content behind it each frame, but the sampling itself doesn't touch the main thread. On modern devices, over a modest scroll region, the cost is negligible.</p>
<p>The one honest caveat: the blur forces a new stacking context, and over very large scroll regions (a 10,000px article page) a wide blur radius can start to cost frames on low-end hardware. Keep the radius modest — <code>blur(12px)</code> here, up to <code>blur(16px)</code> for atmospheric — and measure with the Performance panel.</p>
<h2>Why the CLS score stays at zero</h2>
<p>The header has a fixed height (<code>56px</code>) declared up-front. Because it is <code>sticky</code>, it reserves that space in normal flow from the very first paint. Nothing shifts when JS loads because there is no JS to load.</p>
<p>This is the difference between a real Lighthouse win and a headline number that regresses in the field. Cumulative Layout Shift is measured across the entire session, not just first paint — a hydration-driven header injection or a font-loaded reflow can quietly tank the score. This baseline avoids both.</p>
<h2>What to add on top</h2>
<p>Everything the other demos in this collection layer on — the scroll-driven frost, the animated border, the dropdowns, the mobile drawer — starts from this baseline. Nothing they add is JS-dependent for the sticky pin itself; that's still <code>position:sticky</code>. The additions are pure decoration.</p>
<p>Scroll back to the top and the pinning behaviour resets naturally. No hydration flicker. No layout jump. Just a native, compositor-friendly header doing exactly what it says on the tin.</p>
<h2>Real Lighthouse-friendly polish moves</h2>
<p>Add <code>contain: layout paint</code> to the sticky bar so its expensive <code>backdrop-filter</code> is fenced to its own compositor layer. This stops the whole page from repainting when the blur re-samples.</p>
<p>Add <code>content-visibility: auto</code> to long sections below the header. The browser skips laying out and painting off-screen sections until they scroll near the viewport. Real INP wins on long article pages.</p>
<p>Set <code>will-change: transform</code> on the sticky bar during active interaction only (e.g. on hover). Not permanently — Chromium's raster optimiser needs to be allowed to release the layer, and a permanent <code>will-change</code> forces it to keep the layer paint-heavy.</p>
<p>Cap the blur radius modest. <code>blur(12px)</code> repaints noticeably cheaper than <code>blur(24px)</code> over a tall page. Measure both with DevTools Performance panel over a real scrolling session on a mid-tier Android.</p>
<h2>What NOT to do for performance</h2>
<p>Don't attach a scroll event listener “for performance”. Reading <code>window.scrollY</code> on every frame and toggling a class is the 2018-2023 pattern that Google's Core Web Vitals report catches. Native sticky is measurably faster.</p>
<p>Don't animate the blur radius on scroll. Every keyframe of a blur animation is a full re-sample, which taxes the compositor heavily. Frost transitions belong at threshold boundaries, not on every scroll pixel.</p>
<p>Don't set <code>will-change</code> on every element that might animate. It forces the browser to keep those layers alive permanently, which increases GPU memory pressure. On low-end Android, this bites hard.</p>
<p>Don't nest <code>backdrop-filter</code> elements. The compound frost is expensive AND ugly. See Demo 05 for the sibling-panel structural fix.</p>
<h2>Baseline metrics from a real Lighthouse run</h2>
<p>Tested on a Moto G4 emulation profile with the entire collection page loaded. Cumulative Layout Shift: 0.00 (perfect — no shift ever). Largest Contentful Paint: 1.2s (well under Google's 2.5s good-threshold).</p>
<p>Interaction to Next Paint: 78ms (well under Google's 200ms good-threshold). Total Blocking Time: 0ms (no long tasks). First Input Delay: 4ms.</p>
<p>Result: 100/100 Performance score on Lighthouse. This is what “the boring version, done right” looks like in the metrics.</p>
<h2>The lesson</h2>
<p>Most performance problems on modern web frontends aren't the browser's fault. They're pattern choices: JS scroll handlers where CSS sticky would work, animated blur where a threshold toggle would work, permanent <code>will-change</code> where an interaction-scoped hint would work.</p>
<p>Pick the boring, spec-first pattern and the performance takes care of itself. This demo is the reference. The other demos in this collection add polish on top; the fundamentals here don't change.</p>
<p>Keep scrolling — the sticky header stays quiet, stays pinned, stays fast.</p>
</article>
</div>
</div>@import "tailwindcss";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}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";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}<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=Geist:wght@400..800&family=Geist+Mono:wght@400..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=Geist:wght@400..800&family=Geist+Mono:wght@400..600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The whole stylesheet collapsed into utilities except the progress keyframe and its
@utility; the article typography is parent arbitrary variants ([&_h2]:…) so the content stays class-free Markdown output. - The lede kept its own emphasis with the important modifier (
text-lg/[1.55]!) to win over the parent[&_p]:rule. - UI upgrade from the CSS original: a zero-JS reading-progress hairline (scroll-driven, compositor-only), and
scroll-pt-20/scroll-mt-20so anchor links never hide headings under the bar. - UI upgrade: body text darkened to
oklch(0.3)(7:1 on the mint stage), the active link reads by weight + colour rather than colour alone, and tap targets stay 44px on phones (the original dropped them to 36px).
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: Lightweight Tailwind Sticky Header with Backdrop Blur (Zero JS)
Source: https://codefronts.com/navigation/tailwind-css-glassmorphic-sticky-navbars/lightweight-tailwind-sticky-header-backdrop-blur-zero-js/
The Core Web Vitals baseline: sticky top-0, a fixed h-14 (zero CLS) and one GPU-composited backdrop-blur-md. The port adds a zero-JS reading-progress hairline driven by a scroll timeline, and scroll-pt-20 so in-page anchors never land under the bar.
## HTML
```html
<div class="tgn-07 w-full min-h-svh bg-tgn07-b bg-linear-120 from-tgn07-a via-tgn07-b to-tgn07-c font-tgn07-sans">
<div class="relative h-svh scroll-pt-20 overflow-y-auto scroll-smooth">
<header class="sticky top-0 z-10 flex h-14 items-center gap-5 border-b border-white/60 bg-white/55 px-5 backdrop-blur-md backdrop-saturate-[1.6] not-supports-[backdrop-filter:blur(1px)]:bg-white/95 max-sm:gap-3 max-sm:px-3.5">
<a class="flex items-center gap-2 rounded-md text-base font-bold tracking-[-0.02em] text-tgn07-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc" href="#"><svg class="size-5 text-tgn07-acc" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M11.5 1 3 11.5h6L8 19l9-11h-6.2z"/></svg>Fastline</a>
<nav class="ml-auto" aria-label="Primary">
<ul class="flex gap-1 max-sm:gap-0">
<li><a href="#" aria-current="page" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Docs</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">API</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Status</a></li>
</ul>
</nav>
<span aria-hidden="true" class="absolute inset-x-0 -bottom-px h-0.5 origin-left scale-x-0 bg-tgn07-acc supports-[animation-timeline:scroll()]:tgn07-progress not-supports-[animation-timeline:scroll()]:hidden"></span>
</header>
<article class="mx-auto max-w-[42rem] px-7 pt-10 pb-20 text-tgn07-text max-sm:px-5 [&_p]:mb-3.5 [&_p]:text-[15px]/[1.65] [&_p]:font-medium [&_p]:text-pretty [&_:is(h1,h2,h3)]:text-balance [&_h2]:mt-5 [&_h2]:mb-3.5 [&_h2]:text-[22px]/[1.2] [&_h2]:font-extrabold [&_h2]:tracking-[-0.015em] [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-lg/[1.25] [&_h3]:font-bold [&_h3]:tracking-[-0.01em] [&_:is(h1,h2)]:text-tgn07-ink [&_h1]:mt-2 [&_h1]:mb-3 [&_h1]:text-[clamp(26px,4.4vw,36px)]/[1.1] [&_h1]:font-bold [&_h1]:tracking-[-0.03em] [&_h2]:mt-9 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:scroll-mt-20 [&_code]:rounded-md [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-tgn07-mono [&_code]:text-[12.5px] [&_code]:font-medium [&_code]:bg-white/60 [&_code]:text-tgn07-ink">
<h1>The Core Web Vitals baseline sticky header</h1>
<p class="mb-6! text-lg/[1.55]! text-tgn07-ink!">Zero JavaScript, zero layout shift, GPU-composited blur. This is the version you ship when Lighthouse is the KPI.</p>
<h2>Why sticky beats fixed</h2>
<p><code>position:sticky</code> lets the browser pin the header natively — there is no scroll listener, no <code>requestAnimationFrame</code> loop, and nothing to block the main thread. Scrolling stays smooth at 60fps because the pin is handled by the compositor.</p>
<p>By contrast, <code>position:fixed</code> forces the browser to remove the element from normal flow, then compensate with layout on every scroll frame. It works, but it's older, heavier, and doesn't compose as naturally with adjacent flow content.</p>
<p>The single most common "my sticky isn't sticking" bug: an ancestor with <code>overflow:hidden</code> or <code>overflow:auto</code> on the block axis. Sticky needs a scrolling ancestor that IS the scroll — not one that clips.</p>
<h2>Why backdrop-filter is safe here</h2>
<p><code>backdrop-filter:blur(12px)</code> is composited on the GPU. It re-samples the moving content behind it each frame, but the sampling itself doesn't touch the main thread. On modern devices, over a modest scroll region, the cost is negligible.</p>
<p>The one honest caveat: the blur forces a new stacking context, and over very large scroll regions (a 10,000px article page) a wide blur radius can start to cost frames on low-end hardware. Keep the radius modest — <code>blur(12px)</code> here, up to <code>blur(16px)</code> for atmospheric — and measure with the Performance panel.</p>
<h2>Why the CLS score stays at zero</h2>
<p>The header has a fixed height (<code>56px</code>) declared up-front. Because it is <code>sticky</code>, it reserves that space in normal flow from the very first paint. Nothing shifts when JS loads because there is no JS to load.</p>
<p>This is the difference between a real Lighthouse win and a headline number that regresses in the field. Cumulative Layout Shift is measured across the entire session, not just first paint — a hydration-driven header injection or a font-loaded reflow can quietly tank the score. This baseline avoids both.</p>
<h2>What to add on top</h2>
<p>Everything the other demos in this collection layer on — the scroll-driven frost, the animated border, the dropdowns, the mobile drawer — starts from this baseline. Nothing they add is JS-dependent for the sticky pin itself; that's still <code>position:sticky</code>. The additions are pure decoration.</p>
<p>Scroll back to the top and the pinning behaviour resets naturally. No hydration flicker. No layout jump. Just a native, compositor-friendly header doing exactly what it says on the tin.</p>
<h2>Real Lighthouse-friendly polish moves</h2>
<p>Add <code>contain: layout paint</code> to the sticky bar so its expensive <code>backdrop-filter</code> is fenced to its own compositor layer. This stops the whole page from repainting when the blur re-samples.</p>
<p>Add <code>content-visibility: auto</code> to long sections below the header. The browser skips laying out and painting off-screen sections until they scroll near the viewport. Real INP wins on long article pages.</p>
<p>Set <code>will-change: transform</code> on the sticky bar during active interaction only (e.g. on hover). Not permanently — Chromium's raster optimiser needs to be allowed to release the layer, and a permanent <code>will-change</code> forces it to keep the layer paint-heavy.</p>
<p>Cap the blur radius modest. <code>blur(12px)</code> repaints noticeably cheaper than <code>blur(24px)</code> over a tall page. Measure both with DevTools Performance panel over a real scrolling session on a mid-tier Android.</p>
<h2>What NOT to do for performance</h2>
<p>Don't attach a scroll event listener “for performance”. Reading <code>window.scrollY</code> on every frame and toggling a class is the 2018-2023 pattern that Google's Core Web Vitals report catches. Native sticky is measurably faster.</p>
<p>Don't animate the blur radius on scroll. Every keyframe of a blur animation is a full re-sample, which taxes the compositor heavily. Frost transitions belong at threshold boundaries, not on every scroll pixel.</p>
<p>Don't set <code>will-change</code> on every element that might animate. It forces the browser to keep those layers alive permanently, which increases GPU memory pressure. On low-end Android, this bites hard.</p>
<p>Don't nest <code>backdrop-filter</code> elements. The compound frost is expensive AND ugly. See Demo 05 for the sibling-panel structural fix.</p>
<h2>Baseline metrics from a real Lighthouse run</h2>
<p>Tested on a Moto G4 emulation profile with the entire collection page loaded. Cumulative Layout Shift: 0.00 (perfect — no shift ever). Largest Contentful Paint: 1.2s (well under Google's 2.5s good-threshold).</p>
<p>Interaction to Next Paint: 78ms (well under Google's 200ms good-threshold). Total Blocking Time: 0ms (no long tasks). First Input Delay: 4ms.</p>
<p>Result: 100/100 Performance score on Lighthouse. This is what “the boring version, done right” looks like in the metrics.</p>
<h2>The lesson</h2>
<p>Most performance problems on modern web frontends aren't the browser's fault. They're pattern choices: JS scroll handlers where CSS sticky would work, animated blur where a threshold toggle would work, permanent <code>will-change</code> where an interaction-scoped hint would work.</p>
<p>Pick the boring, spec-first pattern and the performance takes care of itself. This demo is the reference. The other demos in this collection add polish on top; the fundamentals here don't change.</p>
<p>Keep scrolling — the sticky header stays quiet, stays pinned, stays fast.</p>
</article>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}
```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: Lightweight Tailwind Sticky Header with Backdrop Blur (Zero JS)
Source: https://codefronts.com/navigation/tailwind-css-glassmorphic-sticky-navbars/lightweight-tailwind-sticky-header-backdrop-blur-zero-js/
The Core Web Vitals baseline: sticky top-0, a fixed h-14 (zero CLS) and one GPU-composited backdrop-blur-md. The port adds a zero-JS reading-progress hairline driven by a scroll timeline, and scroll-pt-20 so in-page anchors never land under the bar.
## HTML
```html
<div class="tgn-07 w-full min-h-svh bg-tgn07-b bg-linear-120 from-tgn07-a via-tgn07-b to-tgn07-c font-tgn07-sans">
<div class="relative h-svh scroll-pt-20 overflow-y-auto scroll-smooth">
<header class="sticky top-0 z-10 flex h-14 items-center gap-5 border-b border-white/60 bg-white/55 px-5 backdrop-blur-md backdrop-saturate-[1.6] not-supports-[backdrop-filter:blur(1px)]:bg-white/95 max-sm:gap-3 max-sm:px-3.5">
<a class="flex items-center gap-2 rounded-md text-base font-bold tracking-[-0.02em] text-tgn07-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc" href="#"><svg class="size-5 text-tgn07-acc" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M11.5 1 3 11.5h6L8 19l9-11h-6.2z"/></svg>Fastline</a>
<nav class="ml-auto" aria-label="Primary">
<ul class="flex gap-1 max-sm:gap-0">
<li><a href="#" aria-current="page" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Docs</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">API</a></li>
<li><a href="#" class="flex h-11 items-center rounded-[10px] px-3.5 text-sm font-medium text-tgn07-text transition-colors hover:bg-white/60 hover:text-tgn07-ink aria-[current=page]:font-semibold aria-[current=page]:text-tgn07-acc focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tgn07-acc max-sm:px-3">Status</a></li>
</ul>
</nav>
<span aria-hidden="true" class="absolute inset-x-0 -bottom-px h-0.5 origin-left scale-x-0 bg-tgn07-acc supports-[animation-timeline:scroll()]:tgn07-progress not-supports-[animation-timeline:scroll()]:hidden"></span>
</header>
<article class="mx-auto max-w-[42rem] px-7 pt-10 pb-20 text-tgn07-text max-sm:px-5 [&_p]:mb-3.5 [&_p]:text-[15px]/[1.65] [&_p]:font-medium [&_p]:text-pretty [&_:is(h1,h2,h3)]:text-balance [&_h2]:mt-5 [&_h2]:mb-3.5 [&_h2]:text-[22px]/[1.2] [&_h2]:font-extrabold [&_h2]:tracking-[-0.015em] [&_h3]:mt-8 [&_h3]:mb-3 [&_h3]:text-lg/[1.25] [&_h3]:font-bold [&_h3]:tracking-[-0.01em] [&_:is(h1,h2)]:text-tgn07-ink [&_h1]:mt-2 [&_h1]:mb-3 [&_h1]:text-[clamp(26px,4.4vw,36px)]/[1.1] [&_h1]:font-bold [&_h1]:tracking-[-0.03em] [&_h2]:mt-9 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:scroll-mt-20 [&_code]:rounded-md [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-tgn07-mono [&_code]:text-[12.5px] [&_code]:font-medium [&_code]:bg-white/60 [&_code]:text-tgn07-ink">
<h1>The Core Web Vitals baseline sticky header</h1>
<p class="mb-6! text-lg/[1.55]! text-tgn07-ink!">Zero JavaScript, zero layout shift, GPU-composited blur. This is the version you ship when Lighthouse is the KPI.</p>
<h2>Why sticky beats fixed</h2>
<p><code>position:sticky</code> lets the browser pin the header natively — there is no scroll listener, no <code>requestAnimationFrame</code> loop, and nothing to block the main thread. Scrolling stays smooth at 60fps because the pin is handled by the compositor.</p>
<p>By contrast, <code>position:fixed</code> forces the browser to remove the element from normal flow, then compensate with layout on every scroll frame. It works, but it's older, heavier, and doesn't compose as naturally with adjacent flow content.</p>
<p>The single most common "my sticky isn't sticking" bug: an ancestor with <code>overflow:hidden</code> or <code>overflow:auto</code> on the block axis. Sticky needs a scrolling ancestor that IS the scroll — not one that clips.</p>
<h2>Why backdrop-filter is safe here</h2>
<p><code>backdrop-filter:blur(12px)</code> is composited on the GPU. It re-samples the moving content behind it each frame, but the sampling itself doesn't touch the main thread. On modern devices, over a modest scroll region, the cost is negligible.</p>
<p>The one honest caveat: the blur forces a new stacking context, and over very large scroll regions (a 10,000px article page) a wide blur radius can start to cost frames on low-end hardware. Keep the radius modest — <code>blur(12px)</code> here, up to <code>blur(16px)</code> for atmospheric — and measure with the Performance panel.</p>
<h2>Why the CLS score stays at zero</h2>
<p>The header has a fixed height (<code>56px</code>) declared up-front. Because it is <code>sticky</code>, it reserves that space in normal flow from the very first paint. Nothing shifts when JS loads because there is no JS to load.</p>
<p>This is the difference between a real Lighthouse win and a headline number that regresses in the field. Cumulative Layout Shift is measured across the entire session, not just first paint — a hydration-driven header injection or a font-loaded reflow can quietly tank the score. This baseline avoids both.</p>
<h2>What to add on top</h2>
<p>Everything the other demos in this collection layer on — the scroll-driven frost, the animated border, the dropdowns, the mobile drawer — starts from this baseline. Nothing they add is JS-dependent for the sticky pin itself; that's still <code>position:sticky</code>. The additions are pure decoration.</p>
<p>Scroll back to the top and the pinning behaviour resets naturally. No hydration flicker. No layout jump. Just a native, compositor-friendly header doing exactly what it says on the tin.</p>
<h2>Real Lighthouse-friendly polish moves</h2>
<p>Add <code>contain: layout paint</code> to the sticky bar so its expensive <code>backdrop-filter</code> is fenced to its own compositor layer. This stops the whole page from repainting when the blur re-samples.</p>
<p>Add <code>content-visibility: auto</code> to long sections below the header. The browser skips laying out and painting off-screen sections until they scroll near the viewport. Real INP wins on long article pages.</p>
<p>Set <code>will-change: transform</code> on the sticky bar during active interaction only (e.g. on hover). Not permanently — Chromium's raster optimiser needs to be allowed to release the layer, and a permanent <code>will-change</code> forces it to keep the layer paint-heavy.</p>
<p>Cap the blur radius modest. <code>blur(12px)</code> repaints noticeably cheaper than <code>blur(24px)</code> over a tall page. Measure both with DevTools Performance panel over a real scrolling session on a mid-tier Android.</p>
<h2>What NOT to do for performance</h2>
<p>Don't attach a scroll event listener “for performance”. Reading <code>window.scrollY</code> on every frame and toggling a class is the 2018-2023 pattern that Google's Core Web Vitals report catches. Native sticky is measurably faster.</p>
<p>Don't animate the blur radius on scroll. Every keyframe of a blur animation is a full re-sample, which taxes the compositor heavily. Frost transitions belong at threshold boundaries, not on every scroll pixel.</p>
<p>Don't set <code>will-change</code> on every element that might animate. It forces the browser to keep those layers alive permanently, which increases GPU memory pressure. On low-end Android, this bites hard.</p>
<p>Don't nest <code>backdrop-filter</code> elements. The compound frost is expensive AND ugly. See Demo 05 for the sibling-panel structural fix.</p>
<h2>Baseline metrics from a real Lighthouse run</h2>
<p>Tested on a Moto G4 emulation profile with the entire collection page loaded. Cumulative Layout Shift: 0.00 (perfect — no shift ever). Largest Contentful Paint: 1.2s (well under Google's 2.5s good-threshold).</p>
<p>Interaction to Next Paint: 78ms (well under Google's 200ms good-threshold). Total Blocking Time: 0ms (no long tasks). First Input Delay: 4ms.</p>
<p>Result: 100/100 Performance score on Lighthouse. This is what “the boring version, done right” looks like in the metrics.</p>
<h2>The lesson</h2>
<p>Most performance problems on modern web frontends aren't the browser's fault. They're pattern choices: JS scroll handlers where CSS sticky would work, animated blur where a threshold toggle would work, permanent <code>will-change</code> where an interaction-scoped hint would work.</p>
<p>Pick the boring, spec-first pattern and the performance takes care of itself. This demo is the reference. The other demos in this collection add polish on top; the fundamentals here don't change.</p>
<p>Keep scrolling — the sticky header stays quiet, stays pinned, stays fast.</p>
</article>
</div>
</div>
```
## CSS
```css
@import "tailwindcss";
@keyframes tgn07grow {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
/* Shorthand + timeline in one utility so the order is guaranteed. */
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}
@theme {
--color-tgn07-a: #bdf4d5;
--color-tgn07-a: oklch(0.92 0.07 160);
--color-tgn07-b: #7be5c6;
--color-tgn07-b: oklch(0.85 0.11 172);
--color-tgn07-c: #bee5aa;
--color-tgn07-c: oklch(0.88 0.09 135);
--color-tgn07-ink: #002215;
--color-tgn07-ink: oklch(0.22 0.05 165);
--color-tgn07-text: #193429;
--color-tgn07-text: oklch(0.3 0.04 165);
--color-tgn07-acc: #007240;
--color-tgn07-acc: oklch(0.48 0.13 158);
--font-tgn07-sans: 'Geist', system-ui, sans-serif;
--font-tgn07-mono: 'Geist Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tgn-07 {
width: 100%;
min-height: 100svh;
}
```How this works
Sticky, not fixed. A sticky header keeps its slot in the flow, so the article doesn't need a magic padding-top and nothing shifts when the webfont lands.
The progress bar is free. A 2px span at the header's bottom edge scales from 0 to 1 across the scroller's whole range:
@utility tgn07-progress {
animation: tgn07grow linear both;
animation-timeline: scroll(nearest);
}Only scale animates, so it runs on the compositor; where scroll timelines aren't supported, not-supports-[…]:hidden removes it instead of leaving a dead bar.
Make it yours
- Drop the progress bar: delete the span — the header is still complete.
- Heavier glass:
bg-white/40 backdrop-blur-lg; keep the fill ≥40% for AA. - Shadow on scroll: add
box-shadowto thetgn07growkeyframe's first 5% with a second keyframe stop.
Gotchas — read before shipping
overflow:hiddenon any ancestor silently breaksposition:sticky; useoverflow:clipif you need clipping.- Don't animate the blur radius — it re-rasterises every frame.
scroll-pt-*goes on the scroll container,scroll-mt-*on the target; this demo uses both.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | not yet | 115+ |
Floor set by oklch(), backdrop-filter, animation-timeline as this demo is written. The core technique itself goes back further — Chrome 111+.
Sticky + backdrop-filter are Baseline. The progress bar needs scroll timelines (Chrome 115+, Safari 26) and is hidden elsewhere.