Adding the JavaScript Toggle
In part 1, we built the structure. Now we will wire up the hamburger button to show/hide the mobile menu using a small JavaScript snippet.
Simple Toggle Script
<script>
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
</script>
Place this script before the closing </body> tag. It toggles the hidden class on the mobile menu element.
Animated Hamburger to X
We can swap the hamburger icon for an X when the menu is open:
<button id="menu-btn" class="md:hidden p-2 rounded-lg hover:bg-gray-100 focus:outline-none">
<!-- Hamburger icon -->
<svg id="icon-open" class="w-6 h-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<!-- Close (X) icon -->
<svg id="icon-close" class="w-6 h-6 text-gray-700 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<script>
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const iconOpen = document.getElementById('icon-open');
const iconClose = document.getElementById('icon-close');
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
iconOpen.classList.toggle('hidden');
iconClose.classList.toggle('hidden');
});
</script>
Adding Smooth Transition
Instead of an instant show/hide, animate the mobile menu with Tailwind's transition utilities and a bit of JavaScript:
<!-- Mobile menu with transition classes -->
<div id="mobile-menu" class="md:hidden overflow-hidden transition-all duration-300 ease-in-out max-h-0">
<ul class="space-y-2 pb-4">
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg">Home</a></li>
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg">About</a></li>
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg">Services</a></li>
<li><a href="#" class="block px-4 py-2 text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 rounded-lg">Contact</a></li>
</ul>
</div>
<script>
const menuBtn = document.getElementById('menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
let isOpen = false;
menuBtn.addEventListener('click', () => {
isOpen = !isOpen;
if (isOpen) {
mobileMenu.style.maxHeight = mobileMenu.scrollHeight + 'px';
} else {
mobileMenu.style.maxHeight = '0';
}
});
</script>
Using max-h-0 and overflow-hidden with transition-all duration-300, the menu slides open smoothly when we set maxHeight to the content height.
Dropdown Menu
Add a dropdown to a desktop nav item:
<li class="relative group">
<a href="#" class="text-gray-700 hover:text-indigo-600 font-medium text-sm flex items-center gap-1">
Services
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</a>
<!-- Dropdown panel -->
<div class="absolute left-0 top-full mt-2 w-48 bg-white rounded-xl shadow-lg border border-gray-100 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
<ul class="py-2">
<li><a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600">Web Design</a></li>
<li><a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600">Development</a></li>
<li><a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600">SEO</a></li>
<li><a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600">Marketing</a></li>
</ul>
</div>
</li>
Key techniques:
- Parent is
relative group - Dropdown uses
absolute left-0 top-fullfor positioning invisible opacity-0hides it;group-hover:visible group-hover:opacity-100shows it- The chevron rotates with
group-hover:rotate-180
Close Menu on Window Resize
<script>
window.addEventListener('resize', () => {
if (window.innerWidth >= 768) {
mobileMenu.style.maxHeight = '0';
isOpen = false;
}
});
</script>
Complete Responsive Nav
Here is the full navbar combining everything from parts 1 and 2:
<nav class="bg-white shadow-sm sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4">
<div class="flex items-center justify-between h-16">
<a href="#" class="text-xl font-bold text-indigo-600">Brand</a>
<ul class="hidden md:flex items-center space-x-8">
<li><a href="#" class="text-gray-700 hover:text-indigo-600 font-medium text-sm">Home</a></li>
<li><a href="#" class="text-gray-700 hover:text-indigo-600 font-medium text-sm">About</a></li>
<li><a href="#" class="text-gray-700 hover:text-indigo-600 font-medium text-sm">Contact</a></li>
</ul>
<button id="menu-btn" class="md:hidden p-2 rounded-lg hover:bg-gray-100">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<div id="mobile-menu" class="md:hidden overflow-hidden transition-all duration-300 max-h-0">
<ul class="space-y-2 pb-4">
<li><a href="#" class="block px-4 py-2 rounded-lg hover:bg-indigo-50">Home</a></li>
<li><a href="#" class="block px-4 py-2 rounded-lg hover:bg-indigo-50">About</a></li>
<li><a href="#" class="block px-4 py-2 rounded-lg hover:bg-indigo-50">Contact</a></li>
</ul>
</div>
</div>
</nav>
Recap
- Toggle the mobile menu by adding/removing
hiddenor manipulatingmaxHeight. - Swap hamburger/X icons by toggling
hiddenon two SVGs. - Use
max-h-0+overflow-hidden+transition-allfor smooth slide animation. - CSS-only dropdowns use
group+group-hover:visible+opacitytransitions.
Next, we will dive into Tailwind transition and animation utilities.