28 CSS Hamburger Menus10 / 28

CSS + JSMIT licensed

CSS Hamburger Menu Morphing to Close Icon Animation

Not a rotate trick — a true shape morph. The burger is one SVG whose line geometry is written in CSS via the d property, and because d: path() is now animatable, the browser interpolates the actual path coordinates from ☰ to ✕. Straight lines bend through genuinely in-between shapes, something transforms can't produce, with zero libraries where GreenSock or Flubber used to be mandatory. Includes the transform-based fallback behind @supports.

Published Updated

Live Demo
Try it

The code

<section class="chm-10" aria-label="SVG path morph hamburger icon demo">
  <div class="chm-10__stage">
    <div class="chm-10__bar">
      <header class="chm-10__head">
        <span class="chm-10__brand">monoline<i>®</i></span>
        <button class="chm-10__btn" type="button" aria-expanded="false" aria-controls="chm10-panel" aria-label="Open menu">
          <svg class="chm-10__ico" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
            <path class="chm-10__top" d="M 3 6 L 21 6"/>
            <path class="chm-10__mid" d="M 3 12 L 21 12"/>
            <path class="chm-10__bot" d="M 3 18 L 21 18"/>
          </svg>
        </button>
      </header>
      <nav class="chm-10__panel" id="chm10-panel" aria-label="Primary">
        <ul>
          <li><a href="#shop">Shop</a></li>
          <li><a href="#lookbook">Lookbook</a></li>
          <li><a href="#stores">Stores</a></li>
          <li><a href="#journal">Journal</a></li>
        </ul>
      </nav>
    </div>
    <p class="chm-10__note" aria-hidden="true">the <code>d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
  </div>
</section>
.chm-10,
.chm-10 *,
.chm-10 *::before,
.chm-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-10 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.97 0.003 90);
  --panel: oklch(1 0 0);
  --edge: oklch(0.88 0.005 90);
  --ink: oklch(0.2 0.01 90);
  --mut: oklch(0.52 0.01 90);
  --acc: oklch(0.62 0.19 30);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-10__stage {
  min-height: 400px;
  border-radius: 18px;
  border: 1px solid var(--edge);
  background: var(--bg);
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-items: center;
  justify-content: center;
}

.chm-10__bar {
  width: min(420px,100%);
  background: var(--panel);
  border: 1.5px solid var(--ink);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 5px 5px 0 oklch(0.2 0.01 90/.1);
}

.chm-10__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 14px 14px 20px;
}

.chm-10__brand {
  font-size: 15.5px;
  font-weight: 700;
  letter-spacing: -.01em;
}

.chm-10__brand i {
  font-style: normal;
  font-size: 10px;
  vertical-align: super;
  color: var(--mut);
}

.chm-10__btn {
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border: 1.5px solid var(--ink);
  border-radius: 12px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,color .2s,box-shadow .2s,translate .15s;
}

.chm-10__btn:hover {
  background: var(--ink);
  color: var(--panel);
}

.chm-10__btn:active {
  translate: 1px 1px;
}

.chm-10__btn:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.chm-10__top,
.chm-10__mid,
.chm-10__bot {
  transition: d .45s cubic-bezier(.65,0,.35,1),opacity .3s;
}

.chm-10__top {
  d: path("M 3 6 L 21 6");
}

.chm-10__mid {
  d: path("M 3 12 L 21 12");
}

.chm-10__bot {
  d: path("M 3 18 L 21 18");
}

[data-open] .chm-10__top {
  d: path("M 5 5 L 19 19");
}

[data-open] .chm-10__mid {
  d: path("M 12 12 L 12 12");
  opacity: 0;
}

[data-open] .chm-10__bot {
  d: path("M 5 19 L 19 5");
}

@supports not (d: path("M0 0h1")) {
  .chm-10__ico path {
    transition: translate .3s,rotate .3s .05s,opacity .2s;
    transform-origin: 12px 12px;
  }

  [data-open] .chm-10__ico .chm-10__top {
    translate: 0 6px;
    rotate: 45deg;
  }

  [data-open] .chm-10__ico .chm-10__mid {
    opacity: 0;
  }

  [data-open] .chm-10__ico .chm-10__bot {
    translate: 0 -6px;
    rotate: -45deg;
  }
}

.chm-10__panel {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .4s cubic-bezier(.2,.7,.2,1),opacity .3s;
}

[data-open] .chm-10__panel {
  grid-template-rows: 1fr;
  opacity: 1;
}

.chm-10__panel ul {
  list-style: none;
  overflow: hidden;
  min-height: 0;
}

.chm-10__panel a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 52px;
  padding: 0 20px;
  border-top: 1.5px solid var(--ink);
  text-decoration: none;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background .15s,color .15s,padding .2s;
}

.chm-10__panel a::after {
  content: "→";
}

.chm-10__panel a:hover,
.chm-10__panel a:focus-visible {
  background: var(--ink);
  color: var(--panel);
  padding-left: 26px;
}

.chm-10__panel a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -4px;
}

.chm-10__note {
  width: min(420px,100%);
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
  text-align: center;
  text-wrap: pretty;
}

.chm-10__note code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.92 0.005 90);
  border-radius: 5px;
  padding: 1.5px 6px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-10 * {
    transition-duration: .01s !important;
  }
}
(function () {
  document.querySelectorAll('.chm-10__bar').forEach(function (bar) {
    if (bar.dataset.bound) return; bar.dataset.bound = '1';
    var btn = bar.querySelector('.chm-10__btn');
    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(); }
    });
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Hamburger Menu 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: CSS Hamburger Menu Morphing to Close Icon Animation
Source: https://codefronts.com/navigation/css-hamburger-menus/hamburger-menu-with-morphing-icon-animations/

Not a rotate trick — a true shape morph. The burger is one SVG whose line geometry is written in CSS via the d property, and because d: path() is now animatable, the browser interpolates the actual path coordinates from ☰ to ✕. Straight lines bend through genuinely in-between shapes, something transforms can't produce, with zero libraries where GreenSock or Flubber used to be mandatory. Includes the transform-based fallback behind @supports.
## HTML
```html
<section class="chm-10" aria-label="SVG path morph hamburger icon demo">
  <div class="chm-10__stage">
    <div class="chm-10__bar">
      <header class="chm-10__head">
        <span class="chm-10__brand">monoline<i>®</i></span>
        <button class="chm-10__btn" type="button" aria-expanded="false" aria-controls="chm10-panel" aria-label="Open menu">
          <svg class="chm-10__ico" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
            <path class="chm-10__top" d="M 3 6 L 21 6"/>
            <path class="chm-10__mid" d="M 3 12 L 21 12"/>
            <path class="chm-10__bot" d="M 3 18 L 21 18"/>
          </svg>
        </button>
      </header>
      <nav class="chm-10__panel" id="chm10-panel" aria-label="Primary">
        <ul>
          <li><a href="#shop">Shop</a></li>
          <li><a href="#lookbook">Lookbook</a></li>
          <li><a href="#stores">Stores</a></li>
          <li><a href="#journal">Journal</a></li>
        </ul>
      </nav>
    </div>
    <p class="chm-10__note" aria-hidden="true">the <code>d</code> attribute itself is transitioning — slow it in DevTools and watch the endpoints travel</p>
  </div>
</section>
```
## CSS
```css
.chm-10,
.chm-10 *,
.chm-10 *::before,
.chm-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-10 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.97 0.003 90);
  --panel: oklch(1 0 0);
  --edge: oklch(0.88 0.005 90);
  --ink: oklch(0.2 0.01 90);
  --mut: oklch(0.52 0.01 90);
  --acc: oklch(0.62 0.19 30);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-10__stage {
  min-height: 400px;
  border-radius: 18px;
  border: 1px solid var(--edge);
  background: var(--bg);
  padding: 26px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-items: center;
  justify-content: center;
}

.chm-10__bar {
  width: min(420px,100%);
  background: var(--panel);
  border: 1.5px solid var(--ink);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 5px 5px 0 oklch(0.2 0.01 90/.1);
}

.chm-10__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 14px 14px 20px;
}

.chm-10__brand {
  font-size: 15.5px;
  font-weight: 700;
  letter-spacing: -.01em;
}

.chm-10__brand i {
  font-style: normal;
  font-size: 10px;
  vertical-align: super;
  color: var(--mut);
}

.chm-10__btn {
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border: 1.5px solid var(--ink);
  border-radius: 12px;
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  transition: background .2s,color .2s,box-shadow .2s,translate .15s;
}

.chm-10__btn:hover {
  background: var(--ink);
  color: var(--panel);
}

.chm-10__btn:active {
  translate: 1px 1px;
}

.chm-10__btn:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

.chm-10__top,
.chm-10__mid,
.chm-10__bot {
  transition: d .45s cubic-bezier(.65,0,.35,1),opacity .3s;
}

.chm-10__top {
  d: path("M 3 6 L 21 6");
}

.chm-10__mid {
  d: path("M 3 12 L 21 12");
}

.chm-10__bot {
  d: path("M 3 18 L 21 18");
}

[data-open] .chm-10__top {
  d: path("M 5 5 L 19 19");
}

[data-open] .chm-10__mid {
  d: path("M 12 12 L 12 12");
  opacity: 0;
}

[data-open] .chm-10__bot {
  d: path("M 5 19 L 19 5");
}

@supports not (d: path("M0 0h1")) {
  .chm-10__ico path {
    transition: translate .3s,rotate .3s .05s,opacity .2s;
    transform-origin: 12px 12px;
  }

  [data-open] .chm-10__ico .chm-10__top {
    translate: 0 6px;
    rotate: 45deg;
  }

  [data-open] .chm-10__ico .chm-10__mid {
    opacity: 0;
  }

  [data-open] .chm-10__ico .chm-10__bot {
    translate: 0 -6px;
    rotate: -45deg;
  }
}

.chm-10__panel {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .4s cubic-bezier(.2,.7,.2,1),opacity .3s;
}

[data-open] .chm-10__panel {
  grid-template-rows: 1fr;
  opacity: 1;
}

.chm-10__panel ul {
  list-style: none;
  overflow: hidden;
  min-height: 0;
}

.chm-10__panel a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 52px;
  padding: 0 20px;
  border-top: 1.5px solid var(--ink);
  text-decoration: none;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background .15s,color .15s,padding .2s;
}

.chm-10__panel a::after {
  content: "→";
}

.chm-10__panel a:hover,
.chm-10__panel a:focus-visible {
  background: var(--ink);
  color: var(--panel);
  padding-left: 26px;
}

.chm-10__panel a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: -4px;
}

.chm-10__note {
  width: min(420px,100%);
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
  text-align: center;
  text-wrap: pretty;
}

.chm-10__note code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.92 0.005 90);
  border-radius: 5px;
  padding: 1.5px 6px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-10 * {
    transition-duration: .01s !important;
  }
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.chm-10__bar').forEach(function (bar) {
    if (bar.dataset.bound) return; bar.dataset.bound = '1';
    var btn = bar.querySelector('.chm-10__btn');
    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

The icon is three <path> elements with NO d attribute in the markup — geometry lives entirely in CSS, which is what makes it transitionable:

.top{ d: path("M 3 6 L 21 6"); }
.mid{ d: path("M 3 12 L 21 12"); }
.bot{ d: path("M 3 18 L 21 18"); }
path{ transition: d .45s cubic-bezier(.65,0,.35,1), opacity .3s; }

[data-open] .top{ d: path("M 5 5 L 19 19"); }   /* becomes \ */
[data-open] .mid{ d: path("M 12 12 L 12 12"); opacity:0; }
[data-open] .bot{ d: path("M 5 19 L 19 5"); }   /* becomes / */

The interpolation rule: start and end paths must have the SAME command list (here M + L in both states). The browser then tweens the coordinates, so mid-transition the top line is a genuine diagonal-in-progress — watch it slowly and you'll see the endpoints travel. The middle line 'morphs' to a zero-length path at the center while fading, so it appears to be absorbed into the X's intersection.

Support is the story's second half. d: path() transitions work in Chrome and Firefox for years, and Safari joined in 18. The fallback costs six lines and degrades to the classic fold:

@supports not (d: path("M0 0h1")) {
  .top,.mid,.bot{ d:none; }        /* markup keeps inline d as backup */
  [data-open] .ico{ /* rotate/translate morph, demo-03 style */ }
}

This demo keeps a real dropdown attached so you see the icon working a menu, not spinning in a vacuum — the panel uses the grid-rows technique from demo 18.

Make it yours

  • Draw ANY same-command end state: morph to a left arrow with M 4 12 L 20 12 / M 4 12 L 10 6 / M 4 12 L 10 18 — the technique generalizes to every icon pair you can author with matching commands.
  • Overshoot personality: cubic-bezier(.3,1.4,.4,1) makes the X snap past and settle — path interpolation tolerates it fine.
  • Stroke style is plain SVG CSS: stroke-width:2 and stroke-linecap:round here; square caps + width 3 reads brutalist instantly.
  • Stagger the three paths with transition-delay:0s/.05s/.1s for a cascading morph instead of a simultaneous one.

Gotchas — read before shipping

  • Command lists must match EXACTLY (M+L → M+L). A d with H or a different segment count doesn't interpolate — the shape just jumps at the end of the duration.
  • Keep inline d attributes in the SVG markup as the no-CSS fallback; CSS d overrides them when supported, and @supports switches cleanly when not.
  • The d property only exists for SVG path elements — you cannot morph a div's border this way; that's clip-path territory.
  • viewBox coordinates are unitless — writing d: path() values with px units silently invalidates the whole declaration.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

CSS d: path() — Chrome 46+, Firefox 97+, Safari 18 (2024). The @supports fallback serves Safari 16–17 the transform morph, so no user sees a static icon.

Search CodeFronts

Loading…