06 / 15
All 15 in 15 Tailwind CSS Navigation Menus
- 01Tailwind CSS Navbar with Animated Hover Underline
- 02Tailwind CSS Dropdown Menu — Hover & Keyboard Accessible
- 03Tailwind CSS Mega Menu Navigation
- 04Tailwind CSS Hamburger Menu with Slide-Out Drawer
- 05Tailwind CSS Full-Screen Overlay Menu
- 06Tailwind CSS Sticky Navbar That Shrinks on Scroll
- 07Tailwind CSS Collapsible Sidebar Navigation
- 08Tailwind CSS Tabs with Animated Sliding Indicator
- 09Tailwind CSS Breadcrumb Navigation Examples
- 10Glassmorphism Navbar in Tailwind CSS
- 11Tailwind CSS Mobile Bottom Navigation Bar with Animated Icons
- 12Tailwind CSS Multi-Level Accordion Sidebar Menu
- 13Tailwind CSS Navbar with Magnetic Hover Effect
- 14Tailwind CSS Neon Glow Navigation Menu
- 15Tailwind CSS Animated Pill Navigation Indicator
Tailwind CSS Sticky Navbar That Shrinks on Scroll
A sticky, frosted header that compresses from 72px to 52px, tightens its logo and buttons and gains a soft shadow once you scroll. An IntersectionObserver sentinel flips one data-shrunk attribute; every visual change is a data-shrunk: or group-data-shrunk/bar: utility. The reading-progress bar is a CSS scroll-driven animation, with a JS fallback only where unsupported.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="tnv-06 relative w-full min-h-svh bg-tnv06-bg font-tnv06-sans text-tnv06-ink antialiased selection:bg-tnv06-acc selection:text-white">
<div id="tnv06-sentinel" aria-hidden="true" class="pointer-events-none absolute top-0 left-0 h-[60px] w-px"></div>
<div id="tnv06-progress" data-tnv06-progress aria-hidden="true" class="fixed inset-x-0 top-0 z-50 h-[3px] origin-left bg-linear-to-r from-tnv06-acc to-tnv06-acc2 [scale:0_1]"></div>
<nav id="tnv06-bar" aria-label="Main" class="group/bar sticky top-0 z-40 flex h-18 items-center justify-between gap-6 border-b border-tnv06-line bg-white/85 px-5 backdrop-blur-lg transition-[height,box-shadow,border-color] duration-300 ease-out data-shrunk:h-13 data-shrunk:border-tnv06-acc/20 data-shrunk:shadow-[0_8px_30px_-12px_var(--color-tnv06-shadow)] sm:px-12 motion-reduce:transition-none">
<a href="#" class="flex origin-left items-center gap-2.5 text-xl font-bold tracking-[-0.02em] transition-transform duration-300 group-data-shrunk/bar:scale-90">
<span class="grid size-7 place-items-center rounded-[50%_0_50%_50%] bg-tnv06-acc transition-[width,height] duration-300 group-data-shrunk/bar:size-6"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.25" aria-hidden="true"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0112 2a8 8 0 018 8.2c0 7.3-8 11.8-8 11.8z"/></svg></span>Verdant
</a>
<ul class="hidden items-center gap-1 text-sm font-semibold text-tnv06-mut md:flex [&_a]:flex [&_a]:h-10 [&_a]:items-center [&_a]:rounded-lg [&_a]:px-3.5 [&_a]:transition-colors [&_a:hover]:bg-tnv06-soft [&_a:hover]:text-tnv06-ink [&_a[aria-current]]:bg-tnv06-soft [&_a[aria-current]]:text-tnv06-acc-ink">
<li><a href="#" aria-current="page">Home</a></li>
<li><a href="#">Features</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="flex items-center gap-2.5">
<a href="#" class="hidden h-10 items-center rounded-lg border-[1.5px] border-tnv06-line px-4 text-sm font-semibold text-tnv06-acc-ink transition-[height,padding,border-color] duration-300 hover:border-tnv06-acc2 group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5 sm:inline-flex">Log in</a>
<a href="#" class="inline-flex h-10 items-center rounded-lg bg-tnv06-acc px-4 text-sm font-semibold text-white shadow-[0_6px_16px_-6px_var(--color-tnv06-acc)] transition-[height,padding,filter] duration-300 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv06-acc group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5">Try free</a>
</div>
</nav>
<main class="mx-auto min-h-[200svh] max-w-3xl px-5 sm:px-12 [&_code]:rounded [&_code]:bg-tnv06-soft [&_code]:px-1.5 [&_code]:font-tnv06-mono [&_code]:text-[0.85em] [&_code]:text-tnv06-acc-ink">
<section class="py-24">
<h1 class="text-[clamp(2.5rem,6vw,4.25rem)] leading-[1.04] font-bold tracking-[-0.045em] text-balance">Sticky nav that <span class="text-tnv06-acc">shrinks on scroll</span></h1>
<p class="mt-5 max-w-xl text-[17px]/[1.7] text-tnv06-mut text-pretty">Scroll down. The bar compresses from 72px to 52px, the logo and buttons tighten, and a shadow lifts it off the page. JS flips one attribute; Tailwind does the rest.</p>
<p class="mt-8 inline-flex items-center gap-2 text-sm font-bold text-tnv06-acc-ink"><svg class="size-[18px] animate-bounce motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>Scroll to trigger</p>
</section>
<div class="grid gap-5 pb-32 [&>article]:rounded-2xl [&>article]:border [&>article]:border-tnv06-line [&>article]:bg-white [&>article]:p-7 [&_h2]:text-lg [&_h2]:font-bold [&_h2]:tracking-[-0.01em] [&_article_p]:mt-2 [&_article_p]:text-[15px]/[1.7] [&_article_p]:text-tnv06-mut">
<article><h2>How it works</h2><p>An <code>IntersectionObserver</code> watches a 60px sentinel at the top of the page. When it scrolls out of view the nav gets <code>data-shrunk</code>, and utilities like <code>data-shrunk:h-13</code> take over.</p></article>
<article><h2>Performance</h2><p>No scroll handler decides the shrink. The progress bar uses <code>animation-timeline: scroll()</code> where supported, so in modern browsers the page scrolls with zero JavaScript running.</p></article>
<article><h2>Accessibility</h2><p>The shrink is purely visual. Focus order, landmarks and the 40px+ hit areas are unchanged, and <code>motion-reduce:</code> removes the transitions entirely.</p></article>
</div>
</main>
</div><div class="tnv-06 relative w-full min-h-svh bg-tnv06-bg font-tnv06-sans text-tnv06-ink antialiased selection:bg-tnv06-acc selection:text-white">
<div id="tnv06-sentinel" aria-hidden="true" class="pointer-events-none absolute top-0 left-0 h-[60px] w-px"></div>
<div id="tnv06-progress" data-tnv06-progress aria-hidden="true" class="fixed inset-x-0 top-0 z-50 h-[3px] origin-left bg-linear-to-r from-tnv06-acc to-tnv06-acc2 [scale:0_1]"></div>
<nav id="tnv06-bar" aria-label="Main" class="group/bar sticky top-0 z-40 flex h-18 items-center justify-between gap-6 border-b border-tnv06-line bg-white/85 px-5 backdrop-blur-lg transition-[height,box-shadow,border-color] duration-300 ease-out data-shrunk:h-13 data-shrunk:border-tnv06-acc/20 data-shrunk:shadow-[0_8px_30px_-12px_var(--color-tnv06-shadow)] sm:px-12 motion-reduce:transition-none">
<a href="#" class="flex origin-left items-center gap-2.5 text-xl font-bold tracking-[-0.02em] transition-transform duration-300 group-data-shrunk/bar:scale-90">
<span class="grid size-7 place-items-center rounded-[50%_0_50%_50%] bg-tnv06-acc transition-[width,height] duration-300 group-data-shrunk/bar:size-6"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.25" aria-hidden="true"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0112 2a8 8 0 018 8.2c0 7.3-8 11.8-8 11.8z"/></svg></span>Verdant
</a>
<ul class="hidden items-center gap-1 text-sm font-semibold text-tnv06-mut md:flex [&_a]:flex [&_a]:h-10 [&_a]:items-center [&_a]:rounded-lg [&_a]:px-3.5 [&_a]:transition-colors [&_a:hover]:bg-tnv06-soft [&_a:hover]:text-tnv06-ink [&_a[aria-current]]:bg-tnv06-soft [&_a[aria-current]]:text-tnv06-acc-ink">
<li><a href="#" aria-current="page">Home</a></li>
<li><a href="#">Features</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="flex items-center gap-2.5">
<a href="#" class="hidden h-10 items-center rounded-lg border-[1.5px] border-tnv06-line px-4 text-sm font-semibold text-tnv06-acc-ink transition-[height,padding,border-color] duration-300 hover:border-tnv06-acc2 group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5 sm:inline-flex">Log in</a>
<a href="#" class="inline-flex h-10 items-center rounded-lg bg-tnv06-acc px-4 text-sm font-semibold text-white shadow-[0_6px_16px_-6px_var(--color-tnv06-acc)] transition-[height,padding,filter] duration-300 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv06-acc group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5">Try free</a>
</div>
</nav>
<main class="mx-auto min-h-[200svh] max-w-3xl px-5 sm:px-12 [&_code]:rounded [&_code]:bg-tnv06-soft [&_code]:px-1.5 [&_code]:font-tnv06-mono [&_code]:text-[0.85em] [&_code]:text-tnv06-acc-ink">
<section class="py-24">
<h1 class="text-[clamp(2.5rem,6vw,4.25rem)] leading-[1.04] font-bold tracking-[-0.045em] text-balance">Sticky nav that <span class="text-tnv06-acc">shrinks on scroll</span></h1>
<p class="mt-5 max-w-xl text-[17px]/[1.7] text-tnv06-mut text-pretty">Scroll down. The bar compresses from 72px to 52px, the logo and buttons tighten, and a shadow lifts it off the page. JS flips one attribute; Tailwind does the rest.</p>
<p class="mt-8 inline-flex items-center gap-2 text-sm font-bold text-tnv06-acc-ink"><svg class="size-[18px] animate-bounce motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>Scroll to trigger</p>
</section>
<div class="grid gap-5 pb-32 [&>article]:rounded-2xl [&>article]:border [&>article]:border-tnv06-line [&>article]:bg-white [&>article]:p-7 [&_h2]:text-lg [&_h2]:font-bold [&_h2]:tracking-[-0.01em] [&_article_p]:mt-2 [&_article_p]:text-[15px]/[1.7] [&_article_p]:text-tnv06-mut">
<article><h2>How it works</h2><p>An <code>IntersectionObserver</code> watches a 60px sentinel at the top of the page. When it scrolls out of view the nav gets <code>data-shrunk</code>, and utilities like <code>data-shrunk:h-13</code> take over.</p></article>
<article><h2>Performance</h2><p>No scroll handler decides the shrink. The progress bar uses <code>animation-timeline: scroll()</code> where supported, so in modern browsers the page scrolls with zero JavaScript running.</p></article>
<article><h2>Accessibility</h2><p>The shrink is purely visual. Focus order, landmarks and the 40px+ hit areas are unchanged, and <code>motion-reduce:</code> removes the transitions entirely.</p></article>
</div>
</main>
</div>@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}(() => {
const bar = document.getElementById('tnv06-bar');
const sentinel = document.getElementById('tnv06-sentinel');
const prog = document.getElementById('tnv06-progress');
if (!bar || bar.dataset.wired) return;
bar.dataset.wired = '1';
new IntersectionObserver(([entry]) => {
bar.toggleAttribute('data-shrunk', !entry.isIntersecting);
}).observe(sentinel);
if (CSS.supports('animation-timeline: scroll()')) return;
let ticking = false;
const update = () => {
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
prog.style.scale = (max > 0 ? Math.min(scrollY / max, 1) : 0) + ' 1';
ticking = false;
};
addEventListener('scroll', () => {
if (!ticking) { ticking = true; requestAnimationFrame(update); }
}, { passive: true });
update();
})();(() => {
const bar = document.getElementById('tnv06-bar');
const sentinel = document.getElementById('tnv06-sentinel');
const prog = document.getElementById('tnv06-progress');
if (!bar || bar.dataset.wired) return;
bar.dataset.wired = '1';
new IntersectionObserver(([entry]) => {
bar.toggleAttribute('data-shrunk', !entry.isIntersecting);
}).observe(sentinel);
if (CSS.supports('animation-timeline: scroll()')) return;
let ticking = false;
const update = () => {
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
prog.style.scale = (max > 0 ? Math.min(scrollY / max, 1) : 0) + ' 1';
ticking = false;
};
addEventListener('scroll', () => {
if (!ticking) { ticking = true; requestAnimationFrame(update); }
}, { passive: true });
update();
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}<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=Figtree:wght@400..800&family=JetBrains+Mono:wght@400;500&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=Figtree:wght@400..800&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.nav-06__bar.nav-06--shrunk .xdescendant rules becamegroup-data-shrunk/bar:utilities; the script togglesdata-shrunkand never touches a class name.- Upgrade from the CSS original: its description promised an IntersectionObserver but the code ran a full handler on every scroll event; the shrink is now decided by an observer, and the progress bar by a CSS scroll timeline.
- UI upgrade: the progress bar animates
scale(compositor-only) instead ofwidth, the logo scales from its left edge so it no longer drifts, and buttons shrink with the bar. - Accessibility upgrade: active link uses
aria-current, accent text darkened toacc-ink(5.4:1), and the bouncing hint stops undermotion-reduce:.
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 CSS Sticky Navbar That Shrinks on Scroll
Source: https://codefronts.com/navigation/tailwind-css-navigation-menus/tailwind-css-sticky-navbar-shrink-on-scroll/
A sticky, frosted header that compresses from 72px to 52px, tightens its logo and buttons and gains a soft shadow once you scroll. An IntersectionObserver sentinel flips one data-shrunk attribute; every visual change is a data-shrunk: or group-data-shrunk/bar: utility. The reading-progress bar is a CSS scroll-driven animation, with a JS fallback only where unsupported.
## HTML
```html
<div class="tnv-06 relative w-full min-h-svh bg-tnv06-bg font-tnv06-sans text-tnv06-ink antialiased selection:bg-tnv06-acc selection:text-white">
<div id="tnv06-sentinel" aria-hidden="true" class="pointer-events-none absolute top-0 left-0 h-[60px] w-px"></div>
<div id="tnv06-progress" data-tnv06-progress aria-hidden="true" class="fixed inset-x-0 top-0 z-50 h-[3px] origin-left bg-linear-to-r from-tnv06-acc to-tnv06-acc2 [scale:0_1]"></div>
<nav id="tnv06-bar" aria-label="Main" class="group/bar sticky top-0 z-40 flex h-18 items-center justify-between gap-6 border-b border-tnv06-line bg-white/85 px-5 backdrop-blur-lg transition-[height,box-shadow,border-color] duration-300 ease-out data-shrunk:h-13 data-shrunk:border-tnv06-acc/20 data-shrunk:shadow-[0_8px_30px_-12px_var(--color-tnv06-shadow)] sm:px-12 motion-reduce:transition-none">
<a href="#" class="flex origin-left items-center gap-2.5 text-xl font-bold tracking-[-0.02em] transition-transform duration-300 group-data-shrunk/bar:scale-90">
<span class="grid size-7 place-items-center rounded-[50%_0_50%_50%] bg-tnv06-acc transition-[width,height] duration-300 group-data-shrunk/bar:size-6"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.25" aria-hidden="true"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0112 2a8 8 0 018 8.2c0 7.3-8 11.8-8 11.8z"/></svg></span>Verdant
</a>
<ul class="hidden items-center gap-1 text-sm font-semibold text-tnv06-mut md:flex [&_a]:flex [&_a]:h-10 [&_a]:items-center [&_a]:rounded-lg [&_a]:px-3.5 [&_a]:transition-colors [&_a:hover]:bg-tnv06-soft [&_a:hover]:text-tnv06-ink [&_a[aria-current]]:bg-tnv06-soft [&_a[aria-current]]:text-tnv06-acc-ink">
<li><a href="#" aria-current="page">Home</a></li>
<li><a href="#">Features</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="flex items-center gap-2.5">
<a href="#" class="hidden h-10 items-center rounded-lg border-[1.5px] border-tnv06-line px-4 text-sm font-semibold text-tnv06-acc-ink transition-[height,padding,border-color] duration-300 hover:border-tnv06-acc2 group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5 sm:inline-flex">Log in</a>
<a href="#" class="inline-flex h-10 items-center rounded-lg bg-tnv06-acc px-4 text-sm font-semibold text-white shadow-[0_6px_16px_-6px_var(--color-tnv06-acc)] transition-[height,padding,filter] duration-300 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv06-acc group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5">Try free</a>
</div>
</nav>
<main class="mx-auto min-h-[200svh] max-w-3xl px-5 sm:px-12 [&_code]:rounded [&_code]:bg-tnv06-soft [&_code]:px-1.5 [&_code]:font-tnv06-mono [&_code]:text-[0.85em] [&_code]:text-tnv06-acc-ink">
<section class="py-24">
<h1 class="text-[clamp(2.5rem,6vw,4.25rem)] leading-[1.04] font-bold tracking-[-0.045em] text-balance">Sticky nav that <span class="text-tnv06-acc">shrinks on scroll</span></h1>
<p class="mt-5 max-w-xl text-[17px]/[1.7] text-tnv06-mut text-pretty">Scroll down. The bar compresses from 72px to 52px, the logo and buttons tighten, and a shadow lifts it off the page. JS flips one attribute; Tailwind does the rest.</p>
<p class="mt-8 inline-flex items-center gap-2 text-sm font-bold text-tnv06-acc-ink"><svg class="size-[18px] animate-bounce motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>Scroll to trigger</p>
</section>
<div class="grid gap-5 pb-32 [&>article]:rounded-2xl [&>article]:border [&>article]:border-tnv06-line [&>article]:bg-white [&>article]:p-7 [&_h2]:text-lg [&_h2]:font-bold [&_h2]:tracking-[-0.01em] [&_article_p]:mt-2 [&_article_p]:text-[15px]/[1.7] [&_article_p]:text-tnv06-mut">
<article><h2>How it works</h2><p>An <code>IntersectionObserver</code> watches a 60px sentinel at the top of the page. When it scrolls out of view the nav gets <code>data-shrunk</code>, and utilities like <code>data-shrunk:h-13</code> take over.</p></article>
<article><h2>Performance</h2><p>No scroll handler decides the shrink. The progress bar uses <code>animation-timeline: scroll()</code> where supported, so in modern browsers the page scrolls with zero JavaScript running.</p></article>
<article><h2>Accessibility</h2><p>The shrink is purely visual. Focus order, landmarks and the 40px+ hit areas are unchanged, and <code>motion-reduce:</code> removes the transitions entirely.</p></article>
</div>
</main>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}
```
## JavaScript
```js
(() => {
const bar = document.getElementById('tnv06-bar');
const sentinel = document.getElementById('tnv06-sentinel');
const prog = document.getElementById('tnv06-progress');
if (!bar || bar.dataset.wired) return;
bar.dataset.wired = '1';
new IntersectionObserver(([entry]) => {
bar.toggleAttribute('data-shrunk', !entry.isIntersecting);
}).observe(sentinel);
if (CSS.supports('animation-timeline: scroll()')) return;
let ticking = false;
const update = () => {
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
prog.style.scale = (max > 0 ? Math.min(scrollY / max, 1) : 0) + ' 1';
ticking = false;
};
addEventListener('scroll', () => {
if (!ticking) { ticking = true; requestAnimationFrame(update); }
}, { passive: true });
update();
})();
```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 CSS Sticky Navbar That Shrinks on Scroll
Source: https://codefronts.com/navigation/tailwind-css-navigation-menus/tailwind-css-sticky-navbar-shrink-on-scroll/
A sticky, frosted header that compresses from 72px to 52px, tightens its logo and buttons and gains a soft shadow once you scroll. An IntersectionObserver sentinel flips one data-shrunk attribute; every visual change is a data-shrunk: or group-data-shrunk/bar: utility. The reading-progress bar is a CSS scroll-driven animation, with a JS fallback only where unsupported.
## HTML
```html
<div class="tnv-06 relative w-full min-h-svh bg-tnv06-bg font-tnv06-sans text-tnv06-ink antialiased selection:bg-tnv06-acc selection:text-white">
<div id="tnv06-sentinel" aria-hidden="true" class="pointer-events-none absolute top-0 left-0 h-[60px] w-px"></div>
<div id="tnv06-progress" data-tnv06-progress aria-hidden="true" class="fixed inset-x-0 top-0 z-50 h-[3px] origin-left bg-linear-to-r from-tnv06-acc to-tnv06-acc2 [scale:0_1]"></div>
<nav id="tnv06-bar" aria-label="Main" class="group/bar sticky top-0 z-40 flex h-18 items-center justify-between gap-6 border-b border-tnv06-line bg-white/85 px-5 backdrop-blur-lg transition-[height,box-shadow,border-color] duration-300 ease-out data-shrunk:h-13 data-shrunk:border-tnv06-acc/20 data-shrunk:shadow-[0_8px_30px_-12px_var(--color-tnv06-shadow)] sm:px-12 motion-reduce:transition-none">
<a href="#" class="flex origin-left items-center gap-2.5 text-xl font-bold tracking-[-0.02em] transition-transform duration-300 group-data-shrunk/bar:scale-90">
<span class="grid size-7 place-items-center rounded-[50%_0_50%_50%] bg-tnv06-acc transition-[width,height] duration-300 group-data-shrunk/bar:size-6"><svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.25" aria-hidden="true"><path d="M12 22s-8-4.5-8-11.8A8 8 0 0112 2a8 8 0 018 8.2c0 7.3-8 11.8-8 11.8z"/></svg></span>Verdant
</a>
<ul class="hidden items-center gap-1 text-sm font-semibold text-tnv06-mut md:flex [&_a]:flex [&_a]:h-10 [&_a]:items-center [&_a]:rounded-lg [&_a]:px-3.5 [&_a]:transition-colors [&_a:hover]:bg-tnv06-soft [&_a:hover]:text-tnv06-ink [&_a[aria-current]]:bg-tnv06-soft [&_a[aria-current]]:text-tnv06-acc-ink">
<li><a href="#" aria-current="page">Home</a></li>
<li><a href="#">Features</a></li>
<li><a href="#">Pricing</a></li>
<li><a href="#">Blog</a></li>
</ul>
<div class="flex items-center gap-2.5">
<a href="#" class="hidden h-10 items-center rounded-lg border-[1.5px] border-tnv06-line px-4 text-sm font-semibold text-tnv06-acc-ink transition-[height,padding,border-color] duration-300 hover:border-tnv06-acc2 group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5 sm:inline-flex">Log in</a>
<a href="#" class="inline-flex h-10 items-center rounded-lg bg-tnv06-acc px-4 text-sm font-semibold text-white shadow-[0_6px_16px_-6px_var(--color-tnv06-acc)] transition-[height,padding,filter] duration-300 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tnv06-acc group-data-shrunk/bar:h-9 group-data-shrunk/bar:px-3.5">Try free</a>
</div>
</nav>
<main class="mx-auto min-h-[200svh] max-w-3xl px-5 sm:px-12 [&_code]:rounded [&_code]:bg-tnv06-soft [&_code]:px-1.5 [&_code]:font-tnv06-mono [&_code]:text-[0.85em] [&_code]:text-tnv06-acc-ink">
<section class="py-24">
<h1 class="text-[clamp(2.5rem,6vw,4.25rem)] leading-[1.04] font-bold tracking-[-0.045em] text-balance">Sticky nav that <span class="text-tnv06-acc">shrinks on scroll</span></h1>
<p class="mt-5 max-w-xl text-[17px]/[1.7] text-tnv06-mut text-pretty">Scroll down. The bar compresses from 72px to 52px, the logo and buttons tighten, and a shadow lifts it off the page. JS flips one attribute; Tailwind does the rest.</p>
<p class="mt-8 inline-flex items-center gap-2 text-sm font-bold text-tnv06-acc-ink"><svg class="size-[18px] animate-bounce motion-reduce:animate-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>Scroll to trigger</p>
</section>
<div class="grid gap-5 pb-32 [&>article]:rounded-2xl [&>article]:border [&>article]:border-tnv06-line [&>article]:bg-white [&>article]:p-7 [&_h2]:text-lg [&_h2]:font-bold [&_h2]:tracking-[-0.01em] [&_article_p]:mt-2 [&_article_p]:text-[15px]/[1.7] [&_article_p]:text-tnv06-mut">
<article><h2>How it works</h2><p>An <code>IntersectionObserver</code> watches a 60px sentinel at the top of the page. When it scrolls out of view the nav gets <code>data-shrunk</code>, and utilities like <code>data-shrunk:h-13</code> take over.</p></article>
<article><h2>Performance</h2><p>No scroll handler decides the shrink. The progress bar uses <code>animation-timeline: scroll()</code> where supported, so in modern browsers the page scrolls with zero JavaScript running.</p></article>
<article><h2>Accessibility</h2><p>The shrink is purely visual. Focus order, landmarks and the 40px+ hit areas are unchanged, and <code>motion-reduce:</code> removes the transitions entirely.</p></article>
</div>
</main>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tnv06-bg: #f0fdf4;
--color-tnv06-bg: oklch(0.982 0.018 155);
--color-tnv06-soft: #ecfdf5;
--color-tnv06-soft: oklch(0.979 0.021 166);
--color-tnv06-line: #d1fae5;
--color-tnv06-line: oklch(0.95 0.052 163);
--color-tnv06-ink: #052e16;
--color-tnv06-ink: oklch(0.266 0.065 153);
--color-tnv06-mut: #4b5563;
--color-tnv06-mut: oklch(0.446 0.026 257);
--color-tnv06-acc: #059669;
--color-tnv06-acc: oklch(0.596 0.145 163);
--color-tnv06-acc2: #34d399;
--color-tnv06-acc2: oklch(0.765 0.177 163);
--color-tnv06-acc-ink: #047857;
--color-tnv06-acc-ink: oklch(0.508 0.118 166);
--color-tnv06-shadow: #05966940;
--color-tnv06-shadow: oklch(0.596 0.145 163 / 0.25);
--font-tnv06-sans: 'Figtree', system-ui, sans-serif;
--font-tnv06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tnv-06 {
width: 100%;
min-height: 100svh;
}
/* Scroll-driven progress: the one thing left in CSS. The animation shorthand resets
animation-timeline, so the pair must be declared together in source order. */
@keyframes tnv06-progress {
from {
scale: 0 1;
}
to {
scale: 1 1;
}
}
@supports (animation-timeline: scroll()) {
.tnv-06 [data-tnv06-progress] {
animation: tnv06-progress linear both;
animation-timeline: scroll(root);
}
}
```
## JavaScript
```js
(() => {
const bar = document.getElementById('tnv06-bar');
const sentinel = document.getElementById('tnv06-sentinel');
const prog = document.getElementById('tnv06-progress');
if (!bar || bar.dataset.wired) return;
bar.dataset.wired = '1';
new IntersectionObserver(([entry]) => {
bar.toggleAttribute('data-shrunk', !entry.isIntersecting);
}).observe(sentinel);
if (CSS.supports('animation-timeline: scroll()')) return;
let ticking = false;
const update = () => {
const doc = document.documentElement;
const max = doc.scrollHeight - doc.clientHeight;
prog.style.scale = (max > 0 ? Math.min(scrollY / max, 1) : 0) + ' 1';
ticking = false;
};
addEventListener('scroll', () => {
if (!ticking) { ticking = true; requestAnimationFrame(update); }
}, { passive: true });
update();
})();
```How this works
Attribute in, utilities out. The only thing JavaScript knows is data-shrunk:
new IntersectionObserver(([e]) =>
bar.toggleAttribute('data-shrunk', !e.isIntersecting)
).observe(sentinel);
<nav class="group/bar h-18 data-shrunk:h-13 data-shrunk:shadow-…">
<a class="group-data-shrunk/bar:scale-90">Verdant</a>Progress without a scroll listener. Where animation-timeline is supported, a 6-line @supports block ties the bar's scale to page scroll and the script returns early. Elsewhere, a rAF-throttled handler does the same job.
Make it yours
- Shrink more:
data-shrunk:h-12; shrink later: move the sentinel toh-[160px]. - Hide on scroll down: toggle a second attribute from scroll direction and add
data-hidden:-translate-y-full. - Swap the progress gradient by editing
--color-tnv06-accand--color-tnv06-acc2.
Gotchas — read before shipping
position: stickybreaks inside any ancestor withoverflow: hiddenor atransform; keep the header a direct child of the scrolling page.- Animating
heighttriggers layout; it is fine for one bar, but don't copy the pattern onto long lists. - Tailwind's
scale-*utilities use the individualscaleproperty, so the fallback writesstyle.scale, nottransform.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | not yet | 115+ |
Floor set by oklch(), animation-timeline as this demo is written. The core technique itself goes back further — Chrome 111+.
Scroll-driven animations: Chrome 115+, Safari 26+; Firefox uses the JS fallback. IntersectionObserver is universal.