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.

Live Demo
Plain CSSTailwind v4
Try it

Build interactive UI like this from scratch: 20 HTML, CSS & JavaScript projects

ZTM · JavaScript Web Projects · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

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…