20 CSS Animated Buttons 14 / 20

CSS Hamburger To Close Button

The classic three-line hamburger menu icon morphs smoothly into an × close icon via CSS transform transitions — no SVG swaps, just rotating and fading three spans.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ab-14">
  <div class="ab-14__nav">
    <span class="ab-14__brand">MyApp</span>
    <button class="ab-14__btn" id="ab-14-btn" aria-label="Toggle navigation" aria-expanded="false">
      <span class="ab-14__bar"></span>
      <span class="ab-14__bar"></span>
      <span class="ab-14__bar"></span>
    </button>
  </div>
  <div class="ab-14__menu" id="ab-14-menu" aria-hidden="true">
    <a class="ab-14__link" href="#">Home</a>
    <a class="ab-14__link" href="#">Features</a>
    <a class="ab-14__link" href="#">Pricing</a>
    <a class="ab-14__link" href="#">Docs</a>
  </div>
</div>
.ab-14,.ab-14 *,.ab-14 *::before,.ab-14 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-14 ::selection{background:#7c3aed;color:#fff}
.ab-14{
  font-family:system-ui,sans-serif;
  background:#0f172a;
  min-height:100vh;
}
.ab-14__nav{
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:1rem 1.5rem;
  border-bottom:1px solid rgba(255,255,255,.07);
}
.ab-14__brand{
  font-size:1.1rem;
  font-weight:800;
  color:#f1f5f9;
  letter-spacing:-.01em;
}
.ab-14__btn{
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:6px;
  width:40px;height:40px;
  background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.1);
  border-radius:8px;
  cursor:pointer;
  outline:none;
  align-items:center;
  transition:background .25s;
}
.ab-14__btn:hover{background:rgba(255,255,255,.1)}
.ab-14__btn.is-open{background:rgba(124,58,237,.2);border-color:rgba(124,58,237,.4)}
.ab-14__bar{
  display:block;
  width:22px;height:2px;
  background:#e2e8f0;
  border-radius:2px;
  transform-origin:center;
  transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .3s,background .3s;
}
.ab-14__btn.is-open .ab-14__bar:nth-child(1){transform:translateY(8px) rotate(45deg);background:#a78bfa}
.ab-14__btn.is-open .ab-14__bar:nth-child(2){opacity:0;transform:scaleX(0)}
.ab-14__btn.is-open .ab-14__bar:nth-child(3){transform:translateY(-8px) rotate(-45deg);background:#a78bfa}
.ab-14__menu{
  display:flex;
  flex-direction:column;
  overflow:hidden;
  max-height:0;
  transition:max-height .4s cubic-bezier(.4,0,.2,1),opacity .3s;
  opacity:0;
  padding:0 1.5rem;
}
.ab-14__menu.is-open{max-height:200px;opacity:1;padding:1rem 1.5rem}
.ab-14__link{
  display:block;
  padding:.6rem 0;
  font-size:.95rem;
  font-weight:600;
  color:#94a3b8;
  text-decoration:none;
  border-bottom:1px solid rgba(255,255,255,.05);
  transition:color .2s;
}
.ab-14__link:last-child{border-bottom:none}
.ab-14__link:hover{color:#f1f5f9}
@media(prefers-reduced-motion:reduce){
  .ab-14__bar,.ab-14__menu{transition:none}
}
(function(){
  var btn=document.getElementById('ab-14-btn');
  var menu=document.getElementById('ab-14-menu');
  if(!btn||!menu)return;
  btn.addEventListener('click',function(){
    var open=btn.classList.toggle('is-open');
    menu.classList.toggle('is-open',open);
    btn.setAttribute('aria-expanded',open.toString());
    menu.setAttribute('aria-hidden',(!open).toString());
  });
})();

How this works

Three span elements inside the button represent the three bars. In the open state they are vertically stacked with equal height: 2px; width: 24px. When the .is-open class is added by JS, the top bar rotates 45deg and translates downward to the centre (translateY(8px) rotate(45deg)), the middle bar fades out via opacity: 0; transform: scaleX(0), and the bottom bar rotates -45deg and translates upward (translateY(-8px) rotate(-45deg)). The two outer bars cross at centre, forming the ×.

All transitions share the same 0.3s cubic-bezier(.4,0,.2,1) easing so the three motions are synchronized. A subtle transform-origin: center on each bar ensures rotation pivots around the bar's midpoint rather than its corner. The button also receives a background-color and border-radius transition so the tap target visually responds to the state change.

Customize

  • Change the bar colour by updating the background on .ab-14__bar — the open state can use a different colour by targeting .ab-14__btn.is-open .ab-14__bar.
  • Widen the bars for a bolder icon by increasing width from 24px to 28px and adjusting the translateY offset to match the new spacing.
  • Add a rotation to the entire button on open by adding transform: rotate(90deg) to .ab-14__btn.is-open for a spinning close effect.
  • Animate the nav menu simultaneously by targeting a sibling element in JS when toggling the class, so the menu slides down as the icon morphs.
  • Use a different close symbol (e.g. an arrow) by changing the top bar to rotate(-45deg) translateY(4px) translateX(-4px) scaleX(.6) instead of a full cross.

Watch out for

  • The translateY value for the cross formation must exactly match the spacing between the bars — if you change the bar gap, recalculate the offset (it equals the gap between bar centres).
  • Adding the class directly in the click handler is synchronous and fires the transition immediately — do not wrap in requestAnimationFrame or the initial state may not have been painted.
  • On touch devices, the :focus-visible ring may appear after tap; add outline: none specifically for touch by checking :not(:focus-visible) selectors for polish.

Browser support

ChromeSafariFirefoxEdge
36+ 9+ 16+ 36+

CSS transforms and transitions are universally supported; the JS toggle pattern works in all modern browsers.

Search CodeFronts

Loading…