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>