25 / 28
All 28 in 28 Tailwind Hamburger Menus
- 01Tailwind Hamburger Menu with Clip-Path Circle Expansion
- 02Tailwind Keyboard Navigable Hamburger Menu with ARIA Attributes
- 03Tailwind Animated 3-Line to X Hamburger Icon Toggle
- 04Tailwind Slide-In Off-Canvas Drawer with Dimmed Backdrop Overlay
- 05Pure Tailwind Checkbox Hack Hamburger Navigation (No JavaScript)
- 06Tailwind Responsive Hamburger Menu with Integrated Search Bar & Social Links
- 07Tailwind Glassmorphism Backdrop Blur Hamburger Navigation
- 08Tailwind Push Page Navigation Menu (Off-Canvas Content Shift)
- 09Pure Tailwind Hamburger Menu Without JavaScript
- 10Tailwind Hamburger Menu Morphing to Close Icon Animation
- 11Tailwind Full Screen Mobile Navigation Overlay
- 12Tailwind Slide Out Sidebar Navigation / Off Canvas Menu
- 13Tailwind Responsive Navbar Mobile Hamburger Toggle Breakpoints
- 14Tailwind Minimalist Floating Hamburger Button
- 15Tailwind CSS Responsive Hamburger Menu Component
- 16Tailwind Rebuild of the Bootstrap 5 Navbar Hamburger Toggle
- 17Tailwind WCAG Accessible Hamburger Menu Example
- 18Pure Tailwind Dropdown Slide Down Mobile Hamburger Menu
- 19Simple 3-Line Tailwind Hamburger Icon Hover Effect
- 20Tailwind Full Screen Blur Overlay Hamburger Menu
- 21Tailwind Mobile Bottom Navigation Bar with Floating Hamburger Toggle
- 22Pure Tailwind Multi-Level Accordion Hamburger Dropdown Menu
- 23Flexbox Navbar Right-Aligned Tailwind Hamburger Icon
- 24Radial Ripple Expand Tailwind Hamburger Menu
- 25Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
- 26Neumorphic Tailwind Hamburger Toggle Button
- 27Brutalist Design Bordered Tailwind Hamburger Menu
- 28Tailwind 3D Flip Canvas Mobile Hamburger Navigation
Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
The shrinking header without a scroll listener: CSS scroll-driven animations bind a keyframe track to scroll position, so the navbar compresses, the wordmark tightens, the burger slims and a frosted border fades in — all proportional to the first 140px. In utilities the animation is a token and its clock is two arbitrary properties, which reads as exactly what it is: this animation's timeline is the scroller.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thb-25 w-full min-h-svh grid justify-center content-start bg-thb25-bg font-thb25-sans text-thb25-ink px-5 py-9" aria-label="Shrinking sticky navbar on scroll demo">
<div class="w-[min(420px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb25-bg border border-thb25-edge">
<div class="h-full overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
<header class="group sticky top-0 z-2 px-5 py-4.5 bg-[oklch(0.985_0.004_250/0.4)] animate-thb25-shrink [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<input type="checkbox" id="thb25-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
<div class="flex items-center justify-between">
<span class="text-[17px] font-extrabold tracking-[0.16em] uppercase animate-thb25-brand [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">Meridian</span>
<label for="thb25-t" class="grid place-items-center w-11.5 h-11.5 rounded-xl border border-thb25-edge bg-white cursor-pointer select-none transition-[background] duration-200 hover:bg-thb25-hover peer-focus-visible:outline-2 peer-focus-visible:outline-thb25-acc peer-focus-visible:outline-offset-2 animate-thb25-btn [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none" aria-hidden="true">
<span class="relative block w-4.5 h-3 animate-thb25-ico [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
</span>
</label>
</div>
<nav aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb25 group-has-checked:opacity-100 group-has-checked:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#journal" class="flex items-center min-h-11 mt-3 px-1 border-y border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Journal</a></li>
<li><a href="#maps" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Maps</a></li>
<li><a href="#expeditions" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Expeditions</a></li>
</ul>
</nav>
</header>
<div class="grid gap-3.5 px-5 pt-4.5 pb-6.5" aria-hidden="true">
<div class="flex flex-col justify-end gap-1 h-50 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.04_235),oklch(0.82_0.07_255)]">
<b class="text-2xl font-bold tracking-[-0.01em]">Scroll ↓</b>
<span class="text-[12.5px] text-thb25-heroink">the header compresses over the first 140px — pure CSS</span>
</div>
<div class="h-2.75 w-[88%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[72%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[80%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[56%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-white border border-thb25-edge"></div>
<div class="h-2.75 w-[84%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[66%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[77%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[49%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-linear-[150deg,oklch(0.93_0.02_200),oklch(0.88_0.04_180)]"></div>
<div class="h-2.75 w-[81%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[59%] rounded-full bg-thb25-line"></div>
</div>
</div>
</div>
</section><section class="thb-25 w-full min-h-svh grid justify-center content-start bg-thb25-bg font-thb25-sans text-thb25-ink px-5 py-9" aria-label="Shrinking sticky navbar on scroll demo">
<div class="w-[min(420px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb25-bg border border-thb25-edge">
<div class="h-full overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
<header class="group sticky top-0 z-2 px-5 py-4.5 bg-[oklch(0.985_0.004_250/0.4)] animate-thb25-shrink [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<input type="checkbox" id="thb25-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
<div class="flex items-center justify-between">
<span class="text-[17px] font-extrabold tracking-[0.16em] uppercase animate-thb25-brand [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">Meridian</span>
<label for="thb25-t" class="grid place-items-center w-11.5 h-11.5 rounded-xl border border-thb25-edge bg-white cursor-pointer select-none transition-[background] duration-200 hover:bg-thb25-hover peer-focus-visible:outline-2 peer-focus-visible:outline-thb25-acc peer-focus-visible:outline-offset-2 animate-thb25-btn [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none" aria-hidden="true">
<span class="relative block w-4.5 h-3 animate-thb25-ico [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
</span>
</label>
</div>
<nav aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb25 group-has-checked:opacity-100 group-has-checked:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#journal" class="flex items-center min-h-11 mt-3 px-1 border-y border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Journal</a></li>
<li><a href="#maps" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Maps</a></li>
<li><a href="#expeditions" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Expeditions</a></li>
</ul>
</nav>
</header>
<div class="grid gap-3.5 px-5 pt-4.5 pb-6.5" aria-hidden="true">
<div class="flex flex-col justify-end gap-1 h-50 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.04_235),oklch(0.82_0.07_255)]">
<b class="text-2xl font-bold tracking-[-0.01em]">Scroll ↓</b>
<span class="text-[12.5px] text-thb25-heroink">the header compresses over the first 140px — pure CSS</span>
</div>
<div class="h-2.75 w-[88%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[72%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[80%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[56%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-white border border-thb25-edge"></div>
<div class="h-2.75 w-[84%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[66%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[77%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[49%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-linear-[150deg,oklch(0.93_0.02_200),oklch(0.88_0.04_180)]"></div>
<div class="h-2.75 w-[81%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[59%] rounded-full bg-thb25-line"></div>
</div>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
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";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
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=Space+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Four
animationdeclarations plus fouranimation-timeline/animation-rangepairs become oneanimate-*token plus two arbitrary properties per element — and the arbitrary properties read as documentation: this animation's clock is the scroller. - Putting
linear bothinside the@themevalue means the fill mode cannot be forgotten, which is the single most common scroll-animation bug. - The keyframes stay in CSS, as they must; the port moves the timing and the clock into the markup and leaves the motion description in the stylesheet.
:checked ~ .row .burger ibecomesgroup-has-checked:from the header, so scroll animation and checkbox state coexist on the same element with no selector gymnastics.scrollbar-gutter:stablehas no utility and stays an arbitrary property on the scroller — without it the sticky header shifts by the scrollbar width when content grows.motion-reduce:animate-noneis stated per animated element rather than as one blanket media query, which is more verbose and means each element documents its own fallback.
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: Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/sticky-tailwind-navbar-shrinking-hamburger-icon-on-scroll/
The shrinking header without a scroll listener: CSS scroll-driven animations bind a keyframe track to scroll position, so the navbar compresses, the wordmark tightens, the burger slims and a frosted border fades in — all proportional to the first 140px. In utilities the animation is a token and its clock is two arbitrary properties, which reads as exactly what it is: this animation's timeline is the scroller.
## HTML
```html
<section class="thb-25 w-full min-h-svh grid justify-center content-start bg-thb25-bg font-thb25-sans text-thb25-ink px-5 py-9" aria-label="Shrinking sticky navbar on scroll demo">
<div class="w-[min(420px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb25-bg border border-thb25-edge">
<div class="h-full overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
<header class="group sticky top-0 z-2 px-5 py-4.5 bg-[oklch(0.985_0.004_250/0.4)] animate-thb25-shrink [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<input type="checkbox" id="thb25-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
<div class="flex items-center justify-between">
<span class="text-[17px] font-extrabold tracking-[0.16em] uppercase animate-thb25-brand [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">Meridian</span>
<label for="thb25-t" class="grid place-items-center w-11.5 h-11.5 rounded-xl border border-thb25-edge bg-white cursor-pointer select-none transition-[background] duration-200 hover:bg-thb25-hover peer-focus-visible:outline-2 peer-focus-visible:outline-thb25-acc peer-focus-visible:outline-offset-2 animate-thb25-btn [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none" aria-hidden="true">
<span class="relative block w-4.5 h-3 animate-thb25-ico [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
</span>
</label>
</div>
<nav aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb25 group-has-checked:opacity-100 group-has-checked:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#journal" class="flex items-center min-h-11 mt-3 px-1 border-y border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Journal</a></li>
<li><a href="#maps" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Maps</a></li>
<li><a href="#expeditions" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Expeditions</a></li>
</ul>
</nav>
</header>
<div class="grid gap-3.5 px-5 pt-4.5 pb-6.5" aria-hidden="true">
<div class="flex flex-col justify-end gap-1 h-50 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.04_235),oklch(0.82_0.07_255)]">
<b class="text-2xl font-bold tracking-[-0.01em]">Scroll ↓</b>
<span class="text-[12.5px] text-thb25-heroink">the header compresses over the first 140px — pure CSS</span>
</div>
<div class="h-2.75 w-[88%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[72%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[80%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[56%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-white border border-thb25-edge"></div>
<div class="h-2.75 w-[84%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[66%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[77%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[49%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-linear-[150deg,oklch(0.93_0.02_200),oklch(0.88_0.04_180)]"></div>
<div class="h-2.75 w-[81%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[59%] rounded-full bg-thb25-line"></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
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: Sticky Tailwind Navbar Shrinking Hamburger Icon on Scroll
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/sticky-tailwind-navbar-shrinking-hamburger-icon-on-scroll/
The shrinking header without a scroll listener: CSS scroll-driven animations bind a keyframe track to scroll position, so the navbar compresses, the wordmark tightens, the burger slims and a frosted border fades in — all proportional to the first 140px. In utilities the animation is a token and its clock is two arbitrary properties, which reads as exactly what it is: this animation's timeline is the scroller.
## HTML
```html
<section class="thb-25 w-full min-h-svh grid justify-center content-start bg-thb25-bg font-thb25-sans text-thb25-ink px-5 py-9" aria-label="Shrinking sticky navbar on scroll demo">
<div class="w-[min(420px,88vw)] h-135 rounded-3xl overflow-hidden bg-thb25-bg border border-thb25-edge">
<div class="h-full overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
<header class="group sticky top-0 z-2 px-5 py-4.5 bg-[oklch(0.985_0.004_250/0.4)] animate-thb25-shrink [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<input type="checkbox" id="thb25-t" class="peer absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">
<div class="flex items-center justify-between">
<span class="text-[17px] font-extrabold tracking-[0.16em] uppercase animate-thb25-brand [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">Meridian</span>
<label for="thb25-t" class="grid place-items-center w-11.5 h-11.5 rounded-xl border border-thb25-edge bg-white cursor-pointer select-none transition-[background] duration-200 hover:bg-thb25-hover peer-focus-visible:outline-2 peer-focus-visible:outline-thb25-acc peer-focus-visible:outline-offset-2 animate-thb25-btn [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none" aria-hidden="true">
<span class="relative block w-4.5 h-3 animate-thb25-ico [animation-timeline:scroll(nearest)] [animation-range:0_140px] motion-reduce:animate-none">
<i class="absolute left-0 top-0 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:translate-y-[5px] group-has-checked:rotate-45 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-[5px] w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[opacity] duration-200 group-has-checked:opacity-0 motion-reduce:transition-none"></i>
<i class="absolute left-0 top-2.5 w-4.5 h-0.5 rounded-sm bg-thb25-ink transition-[translate,rotate] duration-280 group-has-checked:-translate-y-[5px] group-has-checked:-rotate-45 motion-reduce:transition-none"></i>
</span>
</label>
</div>
<nav aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-350 ease-thb25 group-has-checked:opacity-100 group-has-checked:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#journal" class="flex items-center min-h-11 mt-3 px-1 border-y border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Journal</a></li>
<li><a href="#maps" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Maps</a></li>
<li><a href="#expeditions" class="flex items-center min-h-11 px-1 border-b border-[color-mix(in_oklch,var(--color-thb25-edge)_60%,transparent)] text-sm font-semibold text-thb25-mut no-underline transition-[color,padding] duration-150 hover:text-thb25-acc hover:pl-2.5 focus-visible:text-thb25-acc focus-visible:outline-2 focus-visible:outline-thb25-acc focus-visible:-outline-offset-2">Expeditions</a></li>
</ul>
</nav>
</header>
<div class="grid gap-3.5 px-5 pt-4.5 pb-6.5" aria-hidden="true">
<div class="flex flex-col justify-end gap-1 h-50 p-4.5 rounded-2xl bg-linear-[150deg,oklch(0.9_0.04_235),oklch(0.82_0.07_255)]">
<b class="text-2xl font-bold tracking-[-0.01em]">Scroll ↓</b>
<span class="text-[12.5px] text-thb25-heroink">the header compresses over the first 140px — pure CSS</span>
</div>
<div class="h-2.75 w-[88%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[72%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[80%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[56%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-white border border-thb25-edge"></div>
<div class="h-2.75 w-[84%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[66%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[77%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[49%] rounded-full bg-thb25-line"></div>
<div class="h-32.5 rounded-[14px] bg-linear-[150deg,oklch(0.93_0.02_200),oklch(0.88_0.04_180)]"></div>
<div class="h-2.75 w-[81%] rounded-full bg-thb25-line"></div>
<div class="h-2.75 w-[59%] rounded-full bg-thb25-line"></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb25-bg: #fafbfc;
--color-thb25-bg: oklch(0.985 0.004 250);
--color-thb25-edge: #e3e4ea;
--color-thb25-edge: oklch(0.9 0.008 250);
--color-thb25-line: #e8e9ef;
--color-thb25-line: oklch(0.91 0.006 250);
--color-thb25-hover: #f2f3f7;
--color-thb25-hover: oklch(0.96 0.006 250);
--color-thb25-ink: #32354a;
--color-thb25-ink: oklch(0.24 0.018 255);
--color-thb25-mut: #73768c;
--color-thb25-mut: oklch(0.54 0.014 255);
--color-thb25-heroink: #4a5a7d;
--color-thb25-heroink: oklch(0.4 0.05 255);
--color-thb25-acc: #3f6fd0;
--color-thb25-acc: oklch(0.55 0.15 240);
--font-thb25-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb25-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb25: cubic-bezier(0.2, 0.7, 0.2, 1);
--animate-thb25-shrink: thb25-shrink linear both;
--animate-thb25-brand: thb25-brand linear both;
--animate-thb25-btn: thb25-btn linear both;
--animate-thb25-ico: thb25-ico linear both;
}
/* Four keyframe blocks, one per element riding the shared scroll timeline.
Keyframes are rules; the @theme --animate-* entries above only name them. */
@keyframes thb25-shrink {
to {
padding-block: 8px;
background: oklch(0.99 0.003 250 / 0.85);
box-shadow: 0 1px 0 var(--color-thb25-edge), 0 8px 24px -18px oklch(0 0 0 / 0.3);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
}
@keyframes thb25-brand {
to {
font-size: 14.5px;
letter-spacing: 0.1em;
}
}
@keyframes thb25-btn {
to {
width: 38px;
height: 38px;
border-radius: 10px;
}
}
@keyframes thb25-ico {
to {
scale: 0.85;
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.thb-25 {
width: 100%;
min-height: 100svh;
}
```How this works
Three pieces: keyframes, a timeline, a range.
<header class="sticky top-0 z-2
animate-thb25-shrink
[animation-timeline:scroll(nearest)]
[animation-range:0_140px]">scroll(nearest) points the timeline at the nearest scrolling ancestor — this card's own panel here, the page in production, with identical code. The “duration” IS the range: scrolling 70px puts you 50% through the keyframes, scrolling back rewinds. No listener, no rAF, no scroll state in JS, and it cannot jank because the compositor advances it.
Why the animation is a token and the clock is not. --animate-thb25-shrink: thb25-shrink linear both in @theme gives one class that carries timing and fill mode together. animation-timeline and animation-range are not design-scale values — there is no sensible scale of scrollers — so they stay arbitrary properties, sitting next to the animation they drive.
Progressive enhancement is the default. Engines without scroll timelines never advance the animation, so the header simply stays at full size: the feature IS the polish, and the fallback IS the baseline. Nothing to polyfill and no feature query needed.
The burger still works while shrunken, driving a peer-checked: dropdown — and the button never shrinks below 38px, because scroll polish is not a licence to shrink tap targets.
Make it yours
- Shrink distance is
[animation-range:0_140px]. Match it to your hero's height so the header lands compact exactly as content arrives. - Every animated property lives in one
@keyframesblock per element — extend theto{}block to add letter-spacing, a logo cross-fade or an accent line. - Want it stepped rather than proportional? Add
steps(1,end)to the@themeanimation value and it snaps at the range midpoint — the old “scrolled” class behaviour, still no JS. - In production the scroller is the page and
scroll(nearest)already resolves to it; pin a specific one with[animation-timeline:scroll(root)]if the header sits inside nested scrollers. - Retheme through
--color-thb25-acc, which drives the dropdown hover and every focus ring.
Gotchas — read before shipping
- The
bothfill mode is required and lives inside the@themeanimation value. Without it the header snaps back to unstyled the instant scroll leaves the range. position:stickystill needs every ancestor between the bar and the scroller to be overflow-visible. A scroll timeline does not change the classic sticky-killer.- Firefox ships scroll-driven animations behind a flag. Verify the no-animation fallback looks intentional — here it is just a normal sticky header, which is why no feature query is needed.
- Do not shrink the burger below about 38px. Scroll polish is not a reason to break the 44px guidance any further than necessary.
- Arbitrary values cannot contain spaces:
[animation-range:0_140px]uses the underscore escape. - Animating
paddingandfont-sizeis layout work. It is fine here (a few elements, a 140px range) but keep hero parallax to transforms only.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | n/a (flag) | 115+ |
Scroll-driven animations are Chrome/Edge 115+ and Safari 26 (2025); Firefox keeps them behind layout.css.scroll-driven-animations. Non-supporting engines get the identical sticky header at full size.