28 CSS Hamburger Menus03 / 28

CSS + JSMIT licensed

Animated 3-Line to X Hamburger Icon Toggle CSS

Four production-grade line-to-X morphs on one stage — Spin, Squeeze, Arrow and Drop — each a 3-span icon whose entire animation is translate/rotate/opacity with tuned delays, so every variant runs on the compositor at 60fps. Click any tile (they're real aria-pressed toggle buttons), read the timing recipe printed under it, and lift the one that matches your brand's temperament.

Published

Live Demo
Try it

The code

<section class="chm-03" aria-label="Animated hamburger to X icon variants demo">
  <div class="chm-03__stage">
    <header class="chm-03__head"><h2>Line → X morphs</h2><span>click a tile · real aria-pressed buttons</span></header>
    <div class="chm-03__grid">
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__spin" type="button" aria-pressed="false" aria-label="Toggle spin morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Spin</b><code>rotate 180° · fold 45°</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__squeeze" type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Squeeze</b><code>converge → rotate, 2 beats</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__arrow" type="button" aria-pressed="false" aria-label="Toggle arrow morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Arrow</b><code>scale .62 · origin left</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__drop" type="button" aria-pressed="false" aria-label="Toggle drop morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Drop</b><code>mid falls · outers cross late</code></figcaption>
      </figure>
    </div>
  </div>
</section>
.chm-03,
.chm-03 *,
.chm-03 *::before,
.chm-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-03 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.17 0.02 275);
  --card: oklch(0.22 0.025 275);
  --edge: oklch(0.3 0.03 275);
  --ink: oklch(0.95 0.008 275);
  --mut: oklch(0.65 0.02 275);
  --acc: oklch(0.75 0.15 165);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-03__stage {
  border-radius: 20px;
  padding: 24px;
  background: radial-gradient(100% 90% at 50% -20%,oklch(0.26 0.05 285/.7),transparent 60%),var(--bg);
  border: 1px solid var(--edge);
}

.chm-03__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.chm-03__head h2 {
  font-size: 16px;
  letter-spacing: -.01em;
}

.chm-03__head span {
  font-size: 11.5px;
  color: var(--mut);
}

.chm-03__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(150px,100%),1fr));
  gap: 14px;
}

.chm-03__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.chm-03__cell figcaption {
  display: grid;
  justify-items: center;
  gap: 4px;
  text-align: center;
}

.chm-03__cell b {
  font-size: 13px;
}

.chm-03__cell code {
  font: 10.5px ui-monospace,Menlo,Consolas,monospace;
  color: var(--mut);
}

.chm-03__tile {
  width: 100%;
  aspect-ratio: 1.25;
  display: grid;
  place-items: center;
  border: 1px solid var(--edge);
  border-radius: 16px;
  background: linear-gradient(165deg,color-mix(in oklch,var(--card) 88%,white),var(--card) 60%);
  color: var(--ink);
  cursor: pointer;
  transition: border-color .25s,translate .2s;
}

.chm-03__tile:hover {
  border-color: color-mix(in oklch,var(--acc) 55%,var(--edge));
  translate: 0 -2px;
}

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

.chm-03__tile[aria-pressed="true"] {
  border-color: var(--acc);
  color: var(--acc);
}

.chm-03__ico {
  position: relative;
  width: 22px;
  height: 16px;
  display: block;
  transition: rotate .45s cubic-bezier(.3,.9,.35,1);
}

.chm-03__ico i {
  position: absolute;
  left: 0;
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: translate .3s cubic-bezier(.2,.7,.2,1),rotate .3s cubic-bezier(.2,.7,.2,1),opacity .2s,scale .3s cubic-bezier(.2,.7,.2,1);
}

.chm-03__ico i:nth-child(1) {
  top: 0;
}

.chm-03__ico i:nth-child(2) {
  top: 7px;
}

.chm-03__ico i:nth-child(3) {
  top: 14px;
}

.chm-03__spin[aria-pressed="true"] .chm-03__ico {
  rotate: 180deg;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(2) {
  opacity: 0;
  scale: 0 1;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

.chm-03__squeeze i {
  transition: translate .2s cubic-bezier(.55,0,.55,1),rotate .25s cubic-bezier(.2,.7,.2,1) .18s,opacity .15s .08s;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(2) {
  opacity: 0;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

.chm-03__arrow i {
  transform-origin: left center;
}

.chm-03__arrow[aria-pressed="true"] i:nth-child(1) {
  scale: .62 1;
  rotate: -40deg;
  translate: 0 1px;
}

.chm-03__arrow[aria-pressed="true"] i:nth-child(3) {
  scale: .62 1;
  rotate: 40deg;
  translate: 0 -1px;
}

.chm-03__drop i {
  transition: translate .32s cubic-bezier(.5,0,.9,.6),rotate .3s cubic-bezier(.2,.7,.2,1) .16s,opacity .26s ease-in;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(2) {
  translate: 3px 16px;
  rotate: 14deg;
  opacity: 0;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

@media (prefers-reduced-motion: reduce) {
  .chm-03 * {
    transition-duration: .01s !important;
    transition-delay: 0s !important;
  }
}
(function () {
  document.querySelectorAll('.chm-03__tile').forEach(function (btn) {
    if (btn.dataset.bound) return; btn.dataset.bound = '1';
    btn.addEventListener('click', function () {
      btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
    });
  });
})();
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: Animated 3-Line to X Hamburger Icon Toggle CSS
Source: https://codefronts.com/navigation/css-hamburger-menus/animated-3-line-to-x-hamburger-icon-toggle-css/

Four production-grade line-to-X morphs on one stage — Spin, Squeeze, Arrow and Drop — each a 3-span icon whose entire animation is translate/rotate/opacity with tuned delays, so every variant runs on the compositor at 60fps. Click any tile (they're real aria-pressed toggle buttons), read the timing recipe printed under it, and lift the one that matches your brand's temperament.
## HTML
```html
<section class="chm-03" aria-label="Animated hamburger to X icon variants demo">
  <div class="chm-03__stage">
    <header class="chm-03__head"><h2>Line → X morphs</h2><span>click a tile · real aria-pressed buttons</span></header>
    <div class="chm-03__grid">
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__spin" type="button" aria-pressed="false" aria-label="Toggle spin morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Spin</b><code>rotate 180° · fold 45°</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__squeeze" type="button" aria-pressed="false" aria-label="Toggle squeeze morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Squeeze</b><code>converge → rotate, 2 beats</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__arrow" type="button" aria-pressed="false" aria-label="Toggle arrow morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Arrow</b><code>scale .62 · origin left</code></figcaption>
      </figure>
      <figure class="chm-03__cell">
        <button class="chm-03__tile chm-03__drop" type="button" aria-pressed="false" aria-label="Toggle drop morph demo"><span class="chm-03__ico" aria-hidden="true"><i></i><i></i><i></i></span></button>
        <figcaption><b>Drop</b><code>mid falls · outers cross late</code></figcaption>
      </figure>
    </div>
  </div>
</section>
```
## CSS
```css
.chm-03,
.chm-03 *,
.chm-03 *::before,
.chm-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-03 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --bg: oklch(0.17 0.02 275);
  --card: oklch(0.22 0.025 275);
  --edge: oklch(0.3 0.03 275);
  --ink: oklch(0.95 0.008 275);
  --mut: oklch(0.65 0.02 275);
  --acc: oklch(0.75 0.15 165);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-03__stage {
  border-radius: 20px;
  padding: 24px;
  background: radial-gradient(100% 90% at 50% -20%,oklch(0.26 0.05 285/.7),transparent 60%),var(--bg);
  border: 1px solid var(--edge);
}

.chm-03__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.chm-03__head h2 {
  font-size: 16px;
  letter-spacing: -.01em;
}

.chm-03__head span {
  font-size: 11.5px;
  color: var(--mut);
}

.chm-03__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(min(150px,100%),1fr));
  gap: 14px;
}

.chm-03__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.chm-03__cell figcaption {
  display: grid;
  justify-items: center;
  gap: 4px;
  text-align: center;
}

.chm-03__cell b {
  font-size: 13px;
}

.chm-03__cell code {
  font: 10.5px ui-monospace,Menlo,Consolas,monospace;
  color: var(--mut);
}

.chm-03__tile {
  width: 100%;
  aspect-ratio: 1.25;
  display: grid;
  place-items: center;
  border: 1px solid var(--edge);
  border-radius: 16px;
  background: linear-gradient(165deg,color-mix(in oklch,var(--card) 88%,white),var(--card) 60%);
  color: var(--ink);
  cursor: pointer;
  transition: border-color .25s,translate .2s;
}

.chm-03__tile:hover {
  border-color: color-mix(in oklch,var(--acc) 55%,var(--edge));
  translate: 0 -2px;
}

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

.chm-03__tile[aria-pressed="true"] {
  border-color: var(--acc);
  color: var(--acc);
}

.chm-03__ico {
  position: relative;
  width: 22px;
  height: 16px;
  display: block;
  transition: rotate .45s cubic-bezier(.3,.9,.35,1);
}

.chm-03__ico i {
  position: absolute;
  left: 0;
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: translate .3s cubic-bezier(.2,.7,.2,1),rotate .3s cubic-bezier(.2,.7,.2,1),opacity .2s,scale .3s cubic-bezier(.2,.7,.2,1);
}

.chm-03__ico i:nth-child(1) {
  top: 0;
}

.chm-03__ico i:nth-child(2) {
  top: 7px;
}

.chm-03__ico i:nth-child(3) {
  top: 14px;
}

.chm-03__spin[aria-pressed="true"] .chm-03__ico {
  rotate: 180deg;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(2) {
  opacity: 0;
  scale: 0 1;
}

.chm-03__spin[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

.chm-03__squeeze i {
  transition: translate .2s cubic-bezier(.55,0,.55,1),rotate .25s cubic-bezier(.2,.7,.2,1) .18s,opacity .15s .08s;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(2) {
  opacity: 0;
}

.chm-03__squeeze[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

.chm-03__arrow i {
  transform-origin: left center;
}

.chm-03__arrow[aria-pressed="true"] i:nth-child(1) {
  scale: .62 1;
  rotate: -40deg;
  translate: 0 1px;
}

.chm-03__arrow[aria-pressed="true"] i:nth-child(3) {
  scale: .62 1;
  rotate: 40deg;
  translate: 0 -1px;
}

.chm-03__drop i {
  transition: translate .32s cubic-bezier(.5,0,.9,.6),rotate .3s cubic-bezier(.2,.7,.2,1) .16s,opacity .26s ease-in;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(2) {
  translate: 3px 16px;
  rotate: 14deg;
  opacity: 0;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(1) {
  translate: 0 7px;
  rotate: 45deg;
}

.chm-03__drop[aria-pressed="true"] i:nth-child(3) {
  translate: 0 -7px;
  rotate: -45deg;
}

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

## JavaScript
```js
(function () {
  document.querySelectorAll('.chm-03__tile').forEach(function (btn) {
    if (btn.dataset.bound) return; btn.dataset.bound = '1';
    btn.addEventListener('click', function () {
      btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
    });
  });
})();
```

How this works

All four variants share one skeleton: three absolutely-positioned 2px spans in a 22×16px box, so lines can move independently without flexbox reflow:

.ico{ position:relative; width:22px; height:16px; }
.ico i{ position:absolute; left:0; width:22px; height:2px;
  border-radius:2px; background:currentColor;
  transition: translate .3s cubic-bezier(.2,.7,.2,1),
              rotate .3s cubic-bezier(.2,.7,.2,1),
              opacity .2s, scale .3s; }
.ico i:nth-child(1){ top:0 } .ico i:nth-child(2){ top:7px } .ico i:nth-child(3){ top:14px }

A morph is then just per-line end states. Spin — outer lines fold into the diagonal while the whole icon turns 180°, hiding the movement inside the rotation:

[aria-pressed="true"] .spin .ico{ rotate:180deg; }
[aria-pressed="true"] .spin i:nth-child(1){ translate:0 7px; rotate:45deg; }
[aria-pressed="true"] .spin i:nth-child(2){ opacity:0; scale:0 1; }
[aria-pressed="true"] .spin i:nth-child(3){ translate:0 -7px; rotate:-45deg; }

Squeeze converges the outer lines FIRST, then rotates — two transitions on the same property with staggered delays (translate .2s, rotate .25s .18s) reads as two beats. Arrow doesn't close at all: it shortens the outer lines (scale:.62 1 with transform-origin:left) and angles them into a ← back-glyph, the right morph when the burger opens a sub-level. Drop lets gravity take the middle line (translate + rotate + opacity with an eased-in fall) before the outers cross. Individual translate/rotate/scale properties — not one transform string — are what make the per-channel delays possible.

Make it yours

  • Line geometry is three tokens: box 22×16, bar 2px, mid offset 7px. Scale all four numbers together for a chunkier icon; the morph math survives because end-state translates reference the same offsets.
  • Each variant's personality is its easing: swap Spin's curve for cubic-bezier(.3,1.4,.4,1) to add overshoot wobble.
  • aria-pressed is the styling hook ([aria-pressed="true"]), so the same buttons drive a real panel by adding aria-controls — no CSS changes.
  • Only the X-forming variants suit close buttons; keep Arrow for drill-in navigation and pair it with a slide-in submenu.

Gotchas — read before shipping

  • Animate top and the lines stutter — top is a layout property. All movement here is translate/rotate, which is also why the spans are absolutely positioned from a static top.
  • The middle line must exit with opacity + scale:0 1, not width:0 — width animates layout and drags the other lines' left edge if you ever switch to flex stacking.
  • Keep transition on the BASE rule, not the pressed state, or the close animation plays with no easing (the classic one-way-animation bug).
  • A bare icon button MUST carry an accessible name (aria-label) — three decorative spans announce as nothing at all to a screen reader.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Individual transform properties (translate/rotate/scale) are Baseline since Chrome 104 / Safari 14.1 / Firefox 72 — the stated floor is the oklch()/color-mix() palette. On older engines, rewrite end states as transform strings and it runs anywhere.

Techniques used in this demo

Search CodeFronts

Loading…