Making a Mobile Drop-down Menu
Build a functional hamburger menu with JavaScript — toggle visibility, animate the icon, and add smooth slide-down animation.
Making a Mobile Drop-down Menu
In the previous episode, we hid the navigation on mobile and showed a hamburger icon. Now let's make it functional — clicking the hamburger will slide open a drop-down menu with all navigation links.
The HTML Structure
<header class="site-header">
<div class="container">
<div class="header-flex">
<a href="/" class="logo">
<img src="img/logo.png" alt="Logo">
</a>
<button class="menu-toggle"
aria-label="Toggle menu"
aria-expanded="false">
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
<nav class="nav-links">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/services">Services</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>
</nav>
</div>
</div>
</header>
Base Hamburger CSS
/* Hidden on desktop */
.menu-toggle {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 8px;
z-index: 100;
}
.menu-toggle .bar {
display: block;
width: 28px;
height: 3px;
background: #fff;
margin: 5px 0;
border-radius: 2px;
transition: all 0.3s ease;
}
/* Shown on mobile */
@media (max-width: 480px) {
.menu-toggle {
display: block;
}
}
The JavaScript
// script.js
const menuToggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
menuToggle.addEventListener('click', () => {
const isOpen = navLinks.classList.toggle('open');
menuToggle.classList.toggle('active');
// Accessibility: update aria-expanded
menuToggle.setAttribute('aria-expanded', isOpen);
});
// Close menu when a nav link is clicked
navLinks.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
navLinks.classList.remove('open');
menuToggle.classList.remove('active');
menuToggle.setAttribute('aria-expanded', 'false');
});
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!menuToggle.contains(e.target) &&
!navLinks.contains(e.target)) {
navLinks.classList.remove('open');
menuToggle.classList.remove('active');
menuToggle.setAttribute('aria-expanded', 'false');
}
});
Animating the Hamburger to an X
When the menu is open, transform the three bars into a close (X) icon:
/* Top bar: rotate to form \ of the X */
.menu-toggle.active .bar:nth-child(1) {
transform: rotate(45deg) translate(5px, 6px);
}
/* Middle bar: fade out */
.menu-toggle.active .bar:nth-child(2) {
opacity: 0;
transform: translateX(-10px);
}
/* Bottom bar: rotate to form / of the X */
.menu-toggle.active .bar:nth-child(3) {
transform: rotate(-45deg) translate(5px, -6px);
}
Smooth Slide-Down Animation
Instead of an abrupt show/hide, use a smooth slide-down effect:
@media (max-width: 480px) {
.nav-links {
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: #222;
flex-direction: column;
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
}
.nav-links.open {
max-height: 350px; /* Larger than actual content height */
}
.nav-links a {
display: block;
padding: 15px 20px;
color: #fff;
font-size: 16px;
border-bottom: 1px solid #333;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.nav-links.open a {
opacity: 1;
transform: translateY(0);
}
/* Stagger the animation for each link */
.nav-links.open a:nth-child(1) { transition-delay: 0.05s; }
.nav-links.open a:nth-child(2) { transition-delay: 0.10s; }
.nav-links.open a:nth-child(3) { transition-delay: 0.15s; }
.nav-links.open a:nth-child(4) { transition-delay: 0.20s; }
.nav-links.open a:nth-child(5) { transition-delay: 0.25s; }
}
Why max-height Instead of display?
| Approach | Animatable? | Smooth? |
|---|---|---|
display: none → block | ❌ No | Appears instantly |
height: 0 → auto | ❌ No (auto can't be transitioned) | N/A |
max-height: 0 → 350px | ✅ Yes | Smooth slide |
opacity + transform | ✅ Yes | Fade + slide combo |
Accessibility Checklist
aria-label="Toggle menu"on the button — announces the purposearia-expandeddynamically updated — tells screen readers if the menu is open- Nav links are focusable and keyboard-accessible
- Escape key support (optional enhancement):
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
navLinks.classList.remove('open');
menuToggle.classList.remove('active');
menuToggle.setAttribute('aria-expanded', 'false');
menuToggle.focus(); // Return focus to the button
}
});
Key Takeaways
- Use
classList.toggle()to open/close the menu - Animate the hamburger bars into an X using CSS transforms
- Use
max-heightwithtransitionfor smooth slide animation (notdisplay) - Stagger link animations with
transition-delayfor a polished feel - Close the menu on link click, outside click, and Escape key
- Always update
aria-expandedfor accessibility