10 / 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
Tailwind Hamburger Menu Morphing to Close Icon Animation
Not a rotate trick — a true shape morph. The line geometry lives in CSS as d: path(), and because that property is animatable the browser interpolates the actual coordinates from ☰ to ✕. This is also the collection's clearest example of a technique that must NOT become a utility: path data plus its @supports fallback belongs in the stylesheet, and the markup stays utility-only.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thb-10 w-full min-h-svh bg-thb10-bg font-thb10-sans text-thb10-ink px-5 py-9" aria-label="SVG path morph hamburger icon demo">
<div class="min-h-100 flex flex-col items-center justify-center gap-4.5 p-6.5 rounded-[18px] border border-thb10-edge bg-thb10-bg">
<div class="group w-[min(420px,100%)] overflow-hidden rounded-2xl border-[1.5px] border-thb10-ink bg-thb10-panel shadow-[5px_5px_0_oklch(0.2_0.01_90/0.1)]">
<header class="flex items-center justify-between p-3.5 pl-5">
<span class="text-[15.5px] font-bold tracking-[-0.01em]">monoline<i class="not-italic align-super text-[10px] text-thb10-mut">®</i></span>
<button type="button" aria-expanded="false" aria-controls="thb10-panel" aria-label="Open menu" class="grid place-items-center w-11.5 h-11.5 rounded-xl border-[1.5px] border-thb10-ink bg-thb10-panel text-thb10-ink cursor-pointer transition-[background,color,translate] duration-200 hover:bg-thb10-ink hover:text-thb10-panel active:translate-x-px active:translate-y-px focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:outline-offset-3">
<svg class="thb10-ico" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path class="thb10-top fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 6 L 21 6"/>
<path class="thb10-mid fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 12 L 21 12"/>
<path class="thb10-bot fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 18 L 21 18"/>
</svg>
</button>
</header>
<nav id="thb10-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-400 ease-thb10 group-data-open:opacity-100 group-data-open:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#shop" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Shop<span aria-hidden="true">→</span></a></li>
<li><a href="#lookbook" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Lookbook<span aria-hidden="true">→</span></a></li>
<li><a href="#stores" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Stores<span aria-hidden="true">→</span></a></li>
<li><a href="#journal" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Journal<span aria-hidden="true">→</span></a></li>
</ul>
</nav>
</div>
<p class="w-[min(420px,100%)] text-center text-xs leading-relaxed text-thb10-mut text-pretty" aria-hidden="true">the <code class="font-thb10-mono text-[11px] px-1.5 py-0.5 rounded bg-thb10-code">d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
</div>
</section><section class="thb-10 w-full min-h-svh bg-thb10-bg font-thb10-sans text-thb10-ink px-5 py-9" aria-label="SVG path morph hamburger icon demo">
<div class="min-h-100 flex flex-col items-center justify-center gap-4.5 p-6.5 rounded-[18px] border border-thb10-edge bg-thb10-bg">
<div class="group w-[min(420px,100%)] overflow-hidden rounded-2xl border-[1.5px] border-thb10-ink bg-thb10-panel shadow-[5px_5px_0_oklch(0.2_0.01_90/0.1)]">
<header class="flex items-center justify-between p-3.5 pl-5">
<span class="text-[15.5px] font-bold tracking-[-0.01em]">monoline<i class="not-italic align-super text-[10px] text-thb10-mut">®</i></span>
<button type="button" aria-expanded="false" aria-controls="thb10-panel" aria-label="Open menu" class="grid place-items-center w-11.5 h-11.5 rounded-xl border-[1.5px] border-thb10-ink bg-thb10-panel text-thb10-ink cursor-pointer transition-[background,color,translate] duration-200 hover:bg-thb10-ink hover:text-thb10-panel active:translate-x-px active:translate-y-px focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:outline-offset-3">
<svg class="thb10-ico" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path class="thb10-top fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 6 L 21 6"/>
<path class="thb10-mid fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 12 L 21 12"/>
<path class="thb10-bot fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 18 L 21 18"/>
</svg>
</button>
</header>
<nav id="thb10-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-400 ease-thb10 group-data-open:opacity-100 group-data-open:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#shop" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Shop<span aria-hidden="true">→</span></a></li>
<li><a href="#lookbook" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Lookbook<span aria-hidden="true">→</span></a></li>
<li><a href="#stores" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Stores<span aria-hidden="true">→</span></a></li>
<li><a href="#journal" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Journal<span aria-hidden="true">→</span></a></li>
</ul>
</nav>
</div>
<p class="w-[min(420px,100%)] text-center text-xs leading-relaxed text-thb10-mut text-pretty" aria-hidden="true">the <code class="font-thb10-mono text-[11px] px-1.5 py-0.5 rounded bg-thb10-code">d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
</div>
</section>@import "tailwindcss";
@theme {
--color-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
width: 100%;
min-height: 100svh;
}(function () {
document.querySelectorAll('.thb-10 div.group').forEach(function (bar) {
if (bar.dataset.bound) return; bar.dataset.bound = '1';
var btn = bar.querySelector('button[aria-controls="thb10-panel"]');
function set(open) {
bar.toggleAttribute('data-open', open);
btn.setAttribute('aria-expanded', String(open));
btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
}
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
bar.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && bar.hasAttribute('data-open')) { set(false); btn.focus(); }
});
});
})();(function () {
document.querySelectorAll('.thb-10 div.group').forEach(function (bar) {
if (bar.dataset.bound) return; bar.dataset.bound = '1';
var btn = bar.querySelector('button[aria-controls="thb10-panel"]');
function set(open) {
bar.toggleAttribute('data-open', open);
btn.setAttribute('aria-expanded', String(open));
btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
}
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
bar.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && bar.hasAttribute('data-open')) { set(false); btn.focus(); }
});
});
})();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-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
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&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&family=JetBrains+Mono:wght@400;500;600&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The honest counter-example in the collection: this demo keeps MORE CSS than the original, on purpose. Path data and its feature query are rules, not design-scale values, and forcing them into arbitrary utilities would make them unreadable and brittle.
- Everything around the morph still ports cleanly — the frame, the press state, the dropdown, the hover fill are all utilities, and the state hook stays
group-data-open:. - SVG presentation attributes become utilities (
fill-none stroke-current stroke-2), with onlystroke-linecapneeding the bracket form. - The
hover:pl-6.5nudge is the port's small win: the CSS version animatespadding-leftin a rule, and here the resting and hover padding sit side by side. - The
::afterarrow becomes a real<span aria-hidden>, which also fixes a subtle bug in the original — acontentarrow can be picked up by some screen readers as part of the link text. - The dropdown reuses demo 18's
[grid-template-rows:0fr]pair, so both demos teach the same height-animation trick with no shared stylesheet.
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 Hamburger Menu Morphing to Close Icon Animation
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-hamburger-menu-morphing-to-close-icon-animation/
Not a rotate trick — a true shape morph. The line geometry lives in CSS as d: path(), and because that property is animatable the browser interpolates the actual coordinates from ☰ to ✕. This is also the collection's clearest example of a technique that must NOT become a utility: path data plus its @supports fallback belongs in the stylesheet, and the markup stays utility-only.
## HTML
```html
<section class="thb-10 w-full min-h-svh bg-thb10-bg font-thb10-sans text-thb10-ink px-5 py-9" aria-label="SVG path morph hamburger icon demo">
<div class="min-h-100 flex flex-col items-center justify-center gap-4.5 p-6.5 rounded-[18px] border border-thb10-edge bg-thb10-bg">
<div class="group w-[min(420px,100%)] overflow-hidden rounded-2xl border-[1.5px] border-thb10-ink bg-thb10-panel shadow-[5px_5px_0_oklch(0.2_0.01_90/0.1)]">
<header class="flex items-center justify-between p-3.5 pl-5">
<span class="text-[15.5px] font-bold tracking-[-0.01em]">monoline<i class="not-italic align-super text-[10px] text-thb10-mut">®</i></span>
<button type="button" aria-expanded="false" aria-controls="thb10-panel" aria-label="Open menu" class="grid place-items-center w-11.5 h-11.5 rounded-xl border-[1.5px] border-thb10-ink bg-thb10-panel text-thb10-ink cursor-pointer transition-[background,color,translate] duration-200 hover:bg-thb10-ink hover:text-thb10-panel active:translate-x-px active:translate-y-px focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:outline-offset-3">
<svg class="thb10-ico" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path class="thb10-top fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 6 L 21 6"/>
<path class="thb10-mid fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 12 L 21 12"/>
<path class="thb10-bot fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 18 L 21 18"/>
</svg>
</button>
</header>
<nav id="thb10-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-400 ease-thb10 group-data-open:opacity-100 group-data-open:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#shop" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Shop<span aria-hidden="true">→</span></a></li>
<li><a href="#lookbook" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Lookbook<span aria-hidden="true">→</span></a></li>
<li><a href="#stores" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Stores<span aria-hidden="true">→</span></a></li>
<li><a href="#journal" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Journal<span aria-hidden="true">→</span></a></li>
</ul>
</nav>
</div>
<p class="w-[min(420px,100%)] text-center text-xs leading-relaxed text-thb10-mut text-pretty" aria-hidden="true">the <code class="font-thb10-mono text-[11px] px-1.5 py-0.5 rounded bg-thb10-code">d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-10 div.group').forEach(function (bar) {
if (bar.dataset.bound) return; bar.dataset.bound = '1';
var btn = bar.querySelector('button[aria-controls="thb10-panel"]');
function set(open) {
bar.toggleAttribute('data-open', open);
btn.setAttribute('aria-expanded', String(open));
btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
}
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
bar.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && bar.hasAttribute('data-open')) { set(false); btn.focus(); }
});
});
})();
```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 Hamburger Menu Morphing to Close Icon Animation
Source: https://codefronts.com/navigation/tailwind-hamburger-menus/tailwind-hamburger-menu-morphing-to-close-icon-animation/
Not a rotate trick — a true shape morph. The line geometry lives in CSS as d: path(), and because that property is animatable the browser interpolates the actual coordinates from ☰ to ✕. This is also the collection's clearest example of a technique that must NOT become a utility: path data plus its @supports fallback belongs in the stylesheet, and the markup stays utility-only.
## HTML
```html
<section class="thb-10 w-full min-h-svh bg-thb10-bg font-thb10-sans text-thb10-ink px-5 py-9" aria-label="SVG path morph hamburger icon demo">
<div class="min-h-100 flex flex-col items-center justify-center gap-4.5 p-6.5 rounded-[18px] border border-thb10-edge bg-thb10-bg">
<div class="group w-[min(420px,100%)] overflow-hidden rounded-2xl border-[1.5px] border-thb10-ink bg-thb10-panel shadow-[5px_5px_0_oklch(0.2_0.01_90/0.1)]">
<header class="flex items-center justify-between p-3.5 pl-5">
<span class="text-[15.5px] font-bold tracking-[-0.01em]">monoline<i class="not-italic align-super text-[10px] text-thb10-mut">®</i></span>
<button type="button" aria-expanded="false" aria-controls="thb10-panel" aria-label="Open menu" class="grid place-items-center w-11.5 h-11.5 rounded-xl border-[1.5px] border-thb10-ink bg-thb10-panel text-thb10-ink cursor-pointer transition-[background,color,translate] duration-200 hover:bg-thb10-ink hover:text-thb10-panel active:translate-x-px active:translate-y-px focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:outline-offset-3">
<svg class="thb10-ico" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path class="thb10-top fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 6 L 21 6"/>
<path class="thb10-mid fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 12 L 21 12"/>
<path class="thb10-bot fill-none stroke-current stroke-2 [stroke-linecap:round]" d="M 3 18 L 21 18"/>
</svg>
</button>
</header>
<nav id="thb10-panel" aria-label="Primary" class="grid opacity-0 [grid-template-rows:0fr] transition-[grid-template-rows,opacity] duration-400 ease-thb10 group-data-open:opacity-100 group-data-open:[grid-template-rows:1fr] motion-reduce:transition-none">
<ul class="list-none min-h-0 overflow-hidden">
<li><a href="#shop" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Shop<span aria-hidden="true">→</span></a></li>
<li><a href="#lookbook" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Lookbook<span aria-hidden="true">→</span></a></li>
<li><a href="#stores" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Stores<span aria-hidden="true">→</span></a></li>
<li><a href="#journal" class="flex items-center justify-between min-h-13 px-5 border-t-[1.5px] border-thb10-ink no-underline text-[15px] font-bold tracking-[0.02em] uppercase text-thb10-ink transition-[background,color,padding] duration-150 hover:bg-thb10-ink hover:text-thb10-panel hover:pl-6.5 focus-visible:bg-thb10-ink focus-visible:text-thb10-panel focus-visible:outline-2 focus-visible:outline-thb10-acc focus-visible:-outline-offset-4">Journal<span aria-hidden="true">→</span></a></li>
</ul>
</nav>
</div>
<p class="w-[min(420px,100%)] text-center text-xs leading-relaxed text-thb10-mut text-pretty" aria-hidden="true">the <code class="font-thb10-mono text-[11px] px-1.5 py-0.5 rounded bg-thb10-code">d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thb10-bg: #f8f8f5;
--color-thb10-bg: oklch(0.97 0.003 90);
--color-thb10-panel: #ffffff;
--color-thb10-panel: oklch(1 0 0);
--color-thb10-edge: #dededa;
--color-thb10-edge: oklch(0.88 0.005 90);
--color-thb10-code: #e9e9e5;
--color-thb10-code: oklch(0.92 0.005 90);
--color-thb10-ink: #2b2a27;
--color-thb10-ink: oklch(0.2 0.01 90);
--color-thb10-mut: #77756f;
--color-thb10-mut: oklch(0.52 0.01 90);
--color-thb10-acc: #cf5327;
--color-thb10-acc: oklch(0.62 0.19 30);
--font-thb10-sans: 'Space Grotesk', system-ui, sans-serif;
--font-thb10-mono: 'JetBrains Mono', ui-monospace, monospace;
--ease-thb10: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Path geometry stays CSS on purpose: arbitrary values mangle the spaces inside
path data, and the fallback below has to be a feature query anyway. */
.thb-10 .thb10-top,
.thb-10 .thb10-mid,
.thb-10 .thb10-bot {
transition: d .45s cubic-bezier(0.65, 0, 0.35, 1), opacity .3s;
}
.thb-10 .thb10-top {
d: path("M 3 6 L 21 6");
}
.thb-10 .thb10-mid {
d: path("M 3 12 L 21 12");
}
.thb-10 .thb10-bot {
d: path("M 3 18 L 21 18");
}
.thb-10 [data-open] .thb10-top {
d: path("M 5 5 L 19 19");
}
.thb-10 [data-open] .thb10-mid {
d: path("M 12 12 L 12 12");
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
d: path("M 5 19 L 19 5");
}
@supports not (d: path("M0 0h1")) {
.thb-10 .thb10-ico path {
transition: translate .3s, rotate .3s .05s, opacity .2s;
transform-origin: 12px 12px;
}
.thb-10 [data-open] .thb10-top {
translate: 0 6px;
rotate: 45deg;
}
.thb-10 [data-open] .thb10-mid {
opacity: 0;
}
.thb-10 [data-open] .thb10-bot {
translate: 0 -6px;
rotate: -45deg;
}
}
@media (prefers-reduced-motion: reduce) {
.thb-10 .thb10-ico path {
transition-duration: .01s;
}
}
/* 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-10 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.thb-10 div.group').forEach(function (bar) {
if (bar.dataset.bound) return; bar.dataset.bound = '1';
var btn = bar.querySelector('button[aria-controls="thb10-panel"]');
function set(open) {
bar.toggleAttribute('data-open', open);
btn.setAttribute('aria-expanded', String(open));
btn.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
}
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
bar.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && bar.hasAttribute('data-open')) { set(false); btn.focus(); }
});
});
})();
```How this works
Geometry in CSS, state in the markup. The three paths carry no d attribute worth animating; the shape lives in the stylesheet:
.thb10-top { d: path("M 3 6 L 21 6"); }
[data-open] .thb10-top { d: path("M 5 5 L 19 19"); }The interpolation rule is strict: start and end must have the SAME command list (here M + L in both). The browser then tweens the coordinates, so mid-transition the top bar is a genuine diagonal-in-progress — something no rotate can produce. The middle bar collapses to a zero-length path at the centre while fading, so it reads as absorbed into the X's intersection.
Why this is not an arbitrary value. Tailwind's arbitrary syntax turns underscores into spaces, and path data is full of meaningful spaces and commas. [d:path('M_3_6_L_21_6')] is technically writable and genuinely unreadable — and the pattern needs an @supports not (d: path(…)) fallback regardless, which is a rule, not a value. Keeping both in the stylesheet is the correct call, not a concession.
The fallback costs six lines and degrades to the classic transform fold, so Safari 16–17 users see a working icon rather than a static one.
The panel is real. A grid-template-rows dropdown hangs off the same data-open, so the icon is working a menu rather than spinning in a vacuum.
Make it yours
- Draw any same-command end state: a left arrow is
M 4 12 L 20 12/M 4 12 L 10 6/M 4 12 L 10 18. The technique generalises to every icon pair whose commands match. - Overshoot personality: change the transition curve to
cubic-bezier(.3,1.4,.4,1)in the stylesheet — path interpolation tolerates it fine. - Stroke style is utilities:
stroke-2plus[stroke-linecap:round]here;stroke-[3]with square caps reads brutalist instantly. - Stagger the three paths with
transition-delay: 0s / .05s / .1sfor a cascading morph instead of a simultaneous one. - Retheme through
--color-thb10-accand--color-thb10-ink; the frame, the hover fill and the focus ring all read them.
Gotchas — read before shipping
- Command lists must match EXACTLY (
M+L→M+L). A different segment count does not interpolate — the shape jumps at the end of the duration. - Do NOT write path data as an arbitrary value. Underscore-to-space conversion plus commas make it unreadable and fragile; this is a stylesheet job.
- Keep the inline
dattributes in the SVG markup as the no-CSS fallback. CSSdoverrides them when supported, and@supportsswitches cleanly when not. dexists only on SVG<path>. You cannot morph a div's border this way — that isclip-pathterritory.- viewBox coordinates are unitless. Writing
pxinsidepath()silently invalidates the whole declaration. - The dropdown list still needs
min-h-0 overflow-hidden, or the0frtrack refuses to compress.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 18+ | 128+ | 111+ |
CSS d: path() is Chrome 46+, Firefox 97+ and Safari 18 (2024). The @supports fallback serves Safari 16–17 the transform morph, so no user sees a static icon.