Episode 11 of 12

Making a Mobile Menu

Build an interactive hamburger menu with JavaScript — toggle navigation, animate the icon, and handle accessibility.

Making a Mobile Menu

Our mobile layout hides the navigation and shows a hamburger button, but it doesn't do anything yet. Let's add JavaScript to toggle the menu and CSS animations to make it feel polished.

The JavaScript Toggle

Create a file js/script.js and add:

// Mobile Menu Toggle
const hamburger = document.getElementById('hamburger');
const mainNav = document.getElementById('main-nav');

hamburger.addEventListener('click', () => {
    mainNav.classList.toggle('active');
    hamburger.classList.toggle('active');
});

// Close menu when a link is clicked
const navLinks = mainNav.querySelectorAll('a');
navLinks.forEach(link => {
    link.addEventListener('click', () => {
        mainNav.classList.remove('active');
        hamburger.classList.remove('active');
    });
});

How It Works

  1. We select the hamburger button and the nav element
  2. When the hamburger is clicked, we toggle the active class on both elements
  3. The CSS rule #main-nav.active { display: block; } shows the menu
  4. When any nav link is clicked, we remove the active class to close the menu

Animating the Hamburger Icon

When the menu is open, let's transform the three bars into an X (close icon):

/* Base hamburger styles (from earlier) */
.hamburger .bar {
    display: block;
    width: 25px;
    height: 3px;
    background: #fff;
    margin: 5px 0;
    transition: all 0.3s ease;
}

/* When active: top bar rotates down */
.hamburger.active .bar:nth-child(1) {
    transform: rotate(45deg) translate(5px, 6px);
}

/* When active: middle bar disappears */
.hamburger.active .bar:nth-child(2) {
    opacity: 0;
}

/* When active: bottom bar rotates up */
.hamburger.active .bar:nth-child(3) {
    transform: rotate(-45deg) translate(5px, -6px);
}

Adding a Slide-Down Animation

Instead of the menu just appearing, let's add a smooth slide-down effect:

/* Replace display:none/block with height animation */
#main-nav {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
    /* Remove display:none from mobile query */
}

#main-nav.active {
    max-height: 500px;  /* Must be larger than actual content */
}

Note: Update the mobile media query to use max-height: 0 instead of display: none for smooth animation.

Updated Mobile Nav CSS

@media (max-width: 480px) {
    #main-nav {
        position: absolute;
        top: 100%;
        left: 0;
        width: 100%;
        background: #333;
        border-top: 1px solid #444;
        max-height: 0;
        overflow: hidden;
        transition: max-height 0.4s ease;
    }

    #main-nav.active {
        max-height: 400px;
    }
}

Closing the Menu on Outside Click

// Close menu when clicking outside
document.addEventListener('click', (e) => {
    if (!hamburger.contains(e.target) && 
        !mainNav.contains(e.target)) {
        mainNav.classList.remove('active');
        hamburger.classList.remove('active');
    }
});

Accessibility (ARIA Attributes)

// Update ARIA attributes
hamburger.addEventListener('click', () => {
    const isOpen = mainNav.classList.contains('active');
    hamburger.setAttribute('aria-expanded', isOpen);
});

And update the HTML button:

<button id="hamburger" class="hamburger" 
        aria-label="Toggle navigation"
        aria-expanded="false">
    <span class="bar"></span>
    <span class="bar"></span>
    <span class="bar"></span>
</button>

Complete JavaScript File

// js/script.js

const hamburger = document.getElementById('hamburger');
const mainNav = document.getElementById('main-nav');

// Toggle menu
hamburger.addEventListener('click', () => {
    mainNav.classList.toggle('active');
    hamburger.classList.toggle('active');

    // Update ARIA
    const isOpen = mainNav.classList.contains('active');
    hamburger.setAttribute('aria-expanded', isOpen);
});

// Close on link click
mainNav.querySelectorAll('a').forEach(link => {
    link.addEventListener('click', () => {
        mainNav.classList.remove('active');
        hamburger.classList.remove('active');
        hamburger.setAttribute('aria-expanded', 'false');
    });
});

// Close on outside click
document.addEventListener('click', (e) => {
    if (!hamburger.contains(e.target) && 
        !mainNav.contains(e.target)) {
        mainNav.classList.remove('active');
        hamburger.classList.remove('active');
        hamburger.setAttribute('aria-expanded', 'false');
    }
});

Key Takeaways

  • Use classList.toggle() to show/hide the menu
  • The hamburger icon animates to an X using CSS transforms on the three bars
  • Use max-height with transition for a smooth slide-down animation (not display)
  • Close the menu when clicking links or clicking outside
  • Always include ARIA attributes for screen reader accessibility