Home / Blog / CSS Hamburger Menu: Animate 3 Lines Into an X Icon
UI Components 2026-09-23 · 9 min read

CSS Hamburger Menu: Animate 3 Lines Into an X Icon

Build a CSS-only animated hamburger menu button that transforms three lines into an X icon when clicked. Includes mobile nav integration.

The Hamburger Menu: Still a Mobile Navigation Staple

Despite years of debate, the hamburger icon — three horizontal lines stacked vertically — remains one of the most recognizable UI patterns on mobile. Tapping it reveals a navigation drawer; tapping again (now showing an X) closes it. The animation between these two states is a small but meaningful detail that tells users the interface is responsive and polished.

In this guide, we'll build the hamburger-to-X animation in pure CSS using transform, rotate, translate, and opacity transitions. Then we'll wire it to a real mobile navigation drawer using the checkbox hack for a zero-JS version, and a clean JavaScript toggle for the production-ready version.

Building the Hamburger Icon in HTML

We'll represent the three lines as span elements inside a button. Using a real <button> is essential for accessibility — it's focusable by default and announces correctly to screen readers.

<button class="hamburger" aria-label="Toggle navigation" aria-expanded="false" aria-controls="main-nav">
  <span class="bar bar-1"></span>
  <span class="bar bar-2"></span>
  <span class="bar bar-3"></span>
</button>

<nav id="main-nav" class="mobile-nav" aria-hidden="true">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/work">Work</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Styling the Hamburger Button

Each .bar span becomes a horizontal line. The animation works by transforming bar 1 and bar 3 into diagonal lines and hiding bar 2:

.hamburger {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 28px;
  height: 20px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  z-index: 1000;
}

.bar {
  display: block;
  width: 100%;
  height: 2px;
  background-color: #1e293b;
  border-radius: 2px;
  transform-origin: center;
  transition:
    transform 0.3s ease,
    opacity 0.3s ease;
}

/* Active (open) state */
.hamburger.is-open .bar-1 {
  transform: translateY(9px) rotate(45deg);
}

.hamburger.is-open .bar-2 {
  opacity: 0;
  transform: scaleX(0);
}

.hamburger.is-open .bar-3 {
  transform: translateY(-9px) rotate(-45deg);
}

The math for the translateY value is: the height of the button (20px) divided by 2, minus half the bar height (1px) = 9px. This centers the two remaining bars so they cross exactly in the middle to form an X.

JavaScript Toggle (Recommended Approach)

The JavaScript approach is cleaner, more accessible, and easier to integrate with any framework:

const hamburger = document.querySelector('.hamburger');
const nav = document.querySelector('#main-nav');

hamburger.addEventListener('click', () => {
  const isOpen = hamburger.classList.toggle('is-open');

  // Update ARIA attributes for accessibility
  hamburger.setAttribute('aria-expanded', isOpen);
  nav.setAttribute('aria-hidden', !isOpen);

  // Toggle nav visibility
  nav.classList.toggle('is-visible', isOpen);

  // Trap focus inside nav when open (optional but recommended)
  if (isOpen) {
    nav.querySelector('a').focus();
  }
});

// Close with Escape key
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && hamburger.classList.contains('is-open')) {
    hamburger.classList.remove('is-open');
    hamburger.setAttribute('aria-expanded', 'false');
    nav.setAttribute('aria-hidden', 'true');
    nav.classList.remove('is-visible');
    hamburger.focus();
  }
});

CSS-Only Version: The Checkbox Hack

If you need a zero-JavaScript solution, the checkbox hack works by hiding a checkbox input and styling the label as the hamburger button. When the checkbox state changes, CSS sibling selectors update the navigation:

<input type="checkbox" id="nav-toggle" class="nav-checkbox" aria-hidden="true">
<label for="nav-toggle" class="hamburger" aria-label="Toggle navigation">
  <span class="bar bar-1"></span>
  <span class="bar bar-2"></span>
  <span class="bar bar-3"></span>
</label>

<nav class="mobile-nav">...</nav>
/* Hide the actual checkbox */
.nav-checkbox {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* Animate when checked */
.nav-checkbox:checked + .hamburger .bar-1 {
  transform: translateY(9px) rotate(45deg);
}
.nav-checkbox:checked + .hamburger .bar-2 {
  opacity: 0;
  transform: scaleX(0);
}
.nav-checkbox:checked + .hamburger .bar-3 {
  transform: translateY(-9px) rotate(-45deg);
}

/* Show nav when checked */
.nav-checkbox:checked ~ .mobile-nav {
  transform: translateX(0);
  visibility: visible;
}
Note: The checkbox hack has accessibility limitations — screen readers may not announce the state change as a toggle button. The JavaScript approach with aria-expanded is preferred for production sites.

Mobile Navigation Drawer Styles

The hamburger button is only half the story. Here's a slide-in drawer that accompanies it:

.mobile-nav {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  width: min(300px, 85vw);
  background: #fff;
  box-shadow: -4px 0 30px rgba(0,0,0,0.12);
  transform: translateX(100%);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  visibility: hidden;
  z-index: 999;
  padding: 80px 24px 24px;
}

.mobile-nav.is-visible {
  transform: translateX(0);
  visibility: visible;
}

.mobile-nav ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.mobile-nav li + li {
  margin-top: 8px;
}

.mobile-nav a {
  display: block;
  padding: 12px 16px;
  font-size: 1.1rem;
  text-decoration: none;
  color: #1e293b;
  border-radius: 8px;
  transition: background 0.2s;
}

.mobile-nav a:hover,
.mobile-nav a:focus {
  background: #f1f5f9;
}

/* Overlay backdrop */
.nav-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
  z-index: 998;
}

.nav-overlay.is-visible {
  opacity: 1;
  pointer-events: all;
}

Alternative Animation Styles

The classic X transform is just one option. Here are two more popular animation styles:

Style 2: Arrow Morph

.hamburger.is-open .bar-1 {
  transform: translateY(9px) rotate(135deg) scaleX(0.7);
  transform-origin: right center;
}
.hamburger.is-open .bar-3 {
  transform: translateY(-9px) rotate(-135deg) scaleX(0.7);
  transform-origin: right center;
}

Style 3: Squeeze Effect

.hamburger.is-open .bar-1 {
  transform: rotate(45deg) translate(3px, 8px);
}
.hamburger.is-open .bar-2 {
  transform: scaleX(0);
  opacity: 0;
}
.hamburger.is-open .bar-3 {
  transform: rotate(-45deg) translate(3px, -8px);
}

Use our Live HTML/CSS/JS Editor to test these animation variants interactively. Our CSS Formatter can help you clean up the styles before committing them to your project.

Putting It All Together: Full Component

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hamburger Menu Demo</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; }
    body { font-family: system-ui, sans-serif; }

    header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 16px 24px;
      background: #fff;
      box-shadow: 0 1px 4px rgba(0,0,0,0.08);
      position: relative;
      z-index: 1001;
    }

    .logo { font-weight: 700; font-size: 1.2rem; color: #0f172a; }

    .hamburger {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      width: 28px;
      height: 20px;
      background: none;
      border: none;
      cursor: pointer;
      padding: 0;
    }

    .bar {
      display: block;
      width: 100%;
      height: 2px;
      background: #1e293b;
      border-radius: 2px;
      transition: transform 0.3s ease, opacity 0.3s ease;
    }

    .hamburger.is-open .bar-1 { transform: translateY(9px) rotate(45deg); }
    .hamburger.is-open .bar-2 { opacity: 0; transform: scaleX(0); }
    .hamburger.is-open .bar-3 { transform: translateY(-9px) rotate(-45deg); }

    .mobile-nav {
      position: fixed;
      top: 0; right: 0;
      height: 100vh;
      width: min(300px, 85vw);
      background: #fff;
      box-shadow: -4px 0 30px rgba(0,0,0,0.12);
      transform: translateX(100%);
      transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
      visibility: hidden;
      z-index: 999;
      padding: 80px 24px 24px;
    }
    .mobile-nav.is-visible { transform: translateX(0); visibility: visible; }
    .mobile-nav ul { list-style: none; padding: 0; }
    .mobile-nav li + li { margin-top: 8px; }
    .mobile-nav a {
      display: block;
      padding: 12px 16px;
      font-size: 1.05rem;
      text-decoration: none;
      color: #1e293b;
      border-radius: 8px;
    }
    .mobile-nav a:hover { background: #f1f5f9; }
  </style>
</head>
<body>
  <header>
    <span class="logo">MySite</span>
    <button class="hamburger" aria-label="Toggle navigation"
            aria-expanded="false" aria-controls="main-nav">
      <span class="bar bar-1"></span>
      <span class="bar bar-2"></span>
      <span class="bar bar-3"></span>
    </button>
  </header>

  <nav id="main-nav" class="mobile-nav" aria-hidden="true">
    <ul>
      <li><a href="#">Home</a></li>
      <li><a href="#">About</a></li>
      <li><a href="#">Portfolio</a></li>
      <li><a href="#">Contact</a></li>
    </ul>
  </nav>

  <script>
    const btn = document.querySelector('.hamburger');
    const nav = document.querySelector('#main-nav');
    btn.addEventListener('click', () => {
      const open = btn.classList.toggle('is-open');
      btn.setAttribute('aria-expanded', open);
      nav.setAttribute('aria-hidden', !open);
      nav.classList.toggle('is-visible', open);
    });
  </script>
</body>
</html>

Frequently Asked Questions

How do I animate a hamburger menu to an X using CSS?
Use three span elements as the bars. On the active state, translate the top and bottom bars to the center (using translateY equal to half the button height), then rotate them 45° and -45°. Hide the middle bar by setting opacity: 0 and scaleX(0). Use CSS transition on transform and opacity for the animation.
Should I use the checkbox hack or JavaScript for a hamburger menu?
JavaScript is recommended for production. It allows proper ARIA attribute management (aria-expanded, aria-hidden), keyboard navigation (Escape key), focus management, and screen reader announcements. The checkbox hack works without JS but has accessibility limitations since it doesn't update aria-expanded states.
How do I calculate the translateY value for the hamburger-to-X transform?
The value is: (button height / 2) - (bar height / 2). For a 20px tall button with 2px bars: (20/2) - (2/2) = 9px. This moves bars 1 and 3 to the exact vertical center so they cross perfectly when rotated.
How do I make the hamburger menu accessible?
Use a real <button> element with aria-label="Toggle navigation", aria-expanded set to true/false based on state, and aria-controls pointing to the nav element's ID. The nav should have aria-hidden="true" when closed. Also support Escape key to close and return focus to the button.
How do I slide in a mobile navigation drawer with CSS?
Position the nav with position: fixed, top: 0, right: 0, height: 100vh, and a fixed width. Set transform: translateX(100%) to hide it off-screen, and transition: transform 0.35s ease. Toggle a class that sets transform: translateX(0) to slide it in.

Try Our Free HTML CSS Tools

Practice what you've learned with our free online tools — no installation required.

Live HTML/CSS Editor HTML Formatter CSS Formatter