Build a Responsive Navbar with Tailwind CSS
ID | EN

Build a Responsive Navbar with Tailwind CSS

Kamis, 16 Jan 2025

A well-designed Tailwind CSS navbar is essential for any modern website. In this tutorial, you’ll learn how to build a fully responsive navigation bar that works seamlessly across all devices.

Basic Navbar Structure

Start with a simple horizontal navbar layout:

<nav class="bg-white shadow-lg">
  <div class="max-w-7xl mx-auto px-4">
    <div class="flex justify-between items-center h-16">
      <!-- Logo -->
      <a href="/" class="text-2xl font-bold text-gray-800">
        Logo
      </a>
      
      <!-- Navigation Links -->
      <div class="hidden md:flex space-x-8">
        <a href="#" class="text-gray-600 hover:text-gray-900">Home</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">About</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">Services</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">Contact</a>
      </div>
    </div>
  </div>
</nav>

Key Tailwind classes used:

  • max-w-7xl mx-auto — Centers content with max width
  • flex justify-between items-center — Spaces logo and links
  • hidden md:flex — Hides links on mobile, shows on medium screens
  • space-x-8 — Adds horizontal spacing between links

Mobile Hamburger Menu

Add a hamburger button that appears on mobile:

<nav class="bg-white shadow-lg">
  <div class="max-w-7xl mx-auto px-4">
    <div class="flex justify-between items-center h-16">
      <!-- Logo -->
      <a href="/" class="text-2xl font-bold text-gray-800">Logo</a>
      
      <!-- Desktop Navigation -->
      <div class="hidden md:flex space-x-8">
        <a href="#" class="text-gray-600 hover:text-gray-900">Home</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">About</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">Services</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">Contact</a>
      </div>
      
      <!-- Mobile Menu Button -->
      <button id="menu-btn" class="md:hidden flex flex-col space-y-1.5">
        <span class="block w-6 h-0.5 bg-gray-800"></span>
        <span class="block w-6 h-0.5 bg-gray-800"></span>
        <span class="block w-6 h-0.5 bg-gray-800"></span>
      </button>
    </div>
    
    <!-- Mobile Menu -->
    <div id="mobile-menu" class="hidden md:hidden pb-4">
      <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Home</a>
      <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">About</a>
      <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Services</a>
      <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Contact</a>
    </div>
  </div>
</nav>

<script>
  const menuBtn = document.getElementById('menu-btn');
  const mobileMenu = document.getElementById('mobile-menu');
  
  menuBtn.addEventListener('click', () => {
    mobileMenu.classList.toggle('hidden');
  });
</script>

The hamburger icon uses three span elements styled as horizontal lines. The md:hidden class ensures it only appears on mobile devices.

Make the navbar stick to the top when scrolling:

<nav class="bg-white shadow-lg sticky top-0 z-50">
  <!-- navbar content -->
</nav>

For a fixed navbar that’s always visible:

<nav class="bg-white shadow-lg fixed top-0 left-0 right-0 z-50">
  <!-- navbar content -->
</nav>

<!-- Add padding to body to prevent content from hiding behind navbar -->
<div class="pt-16">
  <!-- page content -->
</div>
<nav id="navbar" class="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-transparent">
  <div class="max-w-7xl mx-auto px-4">
    <div class="flex justify-between items-center h-16">
      <a href="/" class="text-2xl font-bold text-white" id="logo">Logo</a>
      <div class="hidden md:flex space-x-8">
        <a href="#" class="text-white hover:text-gray-200">Home</a>
        <a href="#" class="text-white hover:text-gray-200">About</a>
        <a href="#" class="text-white hover:text-gray-200">Contact</a>
      </div>
    </div>
  </div>
</nav>

<script>
  const navbar = document.getElementById('navbar');
  const logo = document.getElementById('logo');
  const links = navbar.querySelectorAll('a:not(#logo)');
  
  window.addEventListener('scroll', () => {
    if (window.scrollY > 50) {
      navbar.classList.remove('bg-transparent');
      navbar.classList.add('bg-white', 'shadow-lg');
      logo.classList.remove('text-white');
      logo.classList.add('text-gray-800');
      links.forEach(link => {
        link.classList.remove('text-white', 'hover:text-gray-200');
        link.classList.add('text-gray-600', 'hover:text-gray-900');
      });
    } else {
      navbar.classList.add('bg-transparent');
      navbar.classList.remove('bg-white', 'shadow-lg');
      logo.classList.add('text-white');
      logo.classList.remove('text-gray-800');
      links.forEach(link => {
        link.classList.add('text-white', 'hover:text-gray-200');
        link.classList.remove('text-gray-600', 'hover:text-gray-900');
      });
    }
  });
</script>

Add dropdown menus for nested navigation:

<nav class="bg-white shadow-lg">
  <div class="max-w-7xl mx-auto px-4">
    <div class="flex justify-between items-center h-16">
      <a href="/" class="text-2xl font-bold text-gray-800">Logo</a>
      
      <div class="hidden md:flex space-x-8">
        <a href="#" class="text-gray-600 hover:text-gray-900">Home</a>
        
        <!-- Dropdown -->
        <div class="relative group">
          <button class="text-gray-600 hover:text-gray-900 flex items-center">
            Services
            <svg class="w-4 h-4 ml-1" 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" />
            </svg>
          </button>
          
          <!-- Dropdown Menu -->
          <div class="absolute left-0 mt-2 w-48 bg-white rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300">
            <a href="#" class="block px-4 py-2 text-gray-600 hover:bg-gray-100">Web Design</a>
            <a href="#" class="block px-4 py-2 text-gray-600 hover:bg-gray-100">Development</a>
            <a href="#" class="block px-4 py-2 text-gray-600 hover:bg-gray-100">SEO</a>
            <a href="#" class="block px-4 py-2 text-gray-600 hover:bg-gray-100">Marketing</a>
          </div>
        </div>
        
        <a href="#" class="text-gray-600 hover:text-gray-900">About</a>
        <a href="#" class="text-gray-600 hover:text-gray-900">Contact</a>
      </div>
    </div>
  </div>
</nav>

The dropdown uses Tailwind’s group and group-hover utilities for CSS-only hover effects. No JavaScript required!

Animation and Transitions

Animated Hamburger Icon

Transform the hamburger into an X when clicked:

<button id="menu-btn" class="md:hidden flex flex-col space-y-1.5 group">
  <span id="line1" class="block w-6 h-0.5 bg-gray-800 transition-all duration-300 origin-center"></span>
  <span id="line2" class="block w-6 h-0.5 bg-gray-800 transition-all duration-300"></span>
  <span id="line3" class="block w-6 h-0.5 bg-gray-800 transition-all duration-300 origin-center"></span>
</button>

<script>
  const menuBtn = document.getElementById('menu-btn');
  const mobileMenu = document.getElementById('mobile-menu');
  const line1 = document.getElementById('line1');
  const line2 = document.getElementById('line2');
  const line3 = document.getElementById('line3');
  let isOpen = false;
  
  menuBtn.addEventListener('click', () => {
    isOpen = !isOpen;
    mobileMenu.classList.toggle('hidden');
    
    if (isOpen) {
      line1.classList.add('rotate-45', 'translate-y-2');
      line2.classList.add('opacity-0');
      line3.classList.add('-rotate-45', '-translate-y-2');
    } else {
      line1.classList.remove('rotate-45', 'translate-y-2');
      line2.classList.remove('opacity-0');
      line3.classList.remove('-rotate-45', '-translate-y-2');
    }
  });
</script>

Slide-Down Mobile Menu

<div id="mobile-menu" class="md:hidden overflow-hidden max-h-0 transition-all duration-300 ease-in-out">
  <div class="pb-4">
    <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Home</a>
    <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">About</a>
    <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Services</a>
    <a href="#" class="block py-2 text-gray-600 hover:text-gray-900">Contact</a>
  </div>
</div>

<script>
  const menuBtn = document.getElementById('menu-btn');
  const mobileMenu = document.getElementById('mobile-menu');
  
  menuBtn.addEventListener('click', () => {
    if (mobileMenu.style.maxHeight) {
      mobileMenu.style.maxHeight = null;
    } else {
      mobileMenu.style.maxHeight = mobileMenu.scrollHeight + 'px';
    }
  });
</script>

React Component Version

Here’s a complete React component with TypeScript:

import { useState, useEffect } from 'react';

interface NavLink {
  label: string;
  href: string;
  children?: NavLink[];
}

const navLinks: NavLink[] = [
  { label: 'Home', href: '/' },
  { 
    label: 'Services', 
    href: '#',
    children: [
      { label: 'Web Design', href: '/services/design' },
      { label: 'Development', href: '/services/development' },
      { label: 'SEO', href: '/services/seo' },
    ]
  },
  { label: 'About', href: '/about' },
  { label: 'Contact', href: '/contact' },
];

export default function Navbar() {
  const [isOpen, setIsOpen] = useState(false);
  const [isScrolled, setIsScrolled] = useState(false);
  const [activeDropdown, setActiveDropdown] = useState<string | null>(null);

  useEffect(() => {
    const handleScroll = () => {
      setIsScrolled(window.scrollY > 50);
    };
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
      isScrolled ? 'bg-white shadow-lg' : 'bg-transparent'
    }`}>
      <div className="max-w-7xl mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          {/* Logo */}
          <a 
            href="/" 
            className={`text-2xl font-bold transition-colors ${
              isScrolled ? 'text-gray-800' : 'text-white'
            }`}
          >
            Logo
          </a>

          {/* Desktop Navigation */}
          <div className="hidden md:flex space-x-8">
            {navLinks.map((link) => (
              <div key={link.label} className="relative group">
                <a
                  href={link.href}
                  className={`flex items-center transition-colors ${
                    isScrolled 
                      ? 'text-gray-600 hover:text-gray-900' 
                      : 'text-white hover:text-gray-200'
                  }`}
                  onMouseEnter={() => link.children && setActiveDropdown(link.label)}
                  onMouseLeave={() => setActiveDropdown(null)}
                >
                  {link.label}
                  {link.children && (
                    <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                    </svg>
                  )}
                </a>
                
                {/* Dropdown */}
                {link.children && (
                  <div 
                    className={`absolute left-0 mt-2 w-48 bg-white rounded-md shadow-lg transition-all duration-300 ${
                      activeDropdown === link.label 
                        ? 'opacity-100 visible' 
                        : 'opacity-0 invisible'
                    }`}
                    onMouseEnter={() => setActiveDropdown(link.label)}
                    onMouseLeave={() => setActiveDropdown(null)}
                  >
                    {link.children.map((child) => (
                      <a
                        key={child.label}
                        href={child.href}
                        className="block px-4 py-2 text-gray-600 hover:bg-gray-100"
                      >
                        {child.label}
                      </a>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </div>

          {/* Mobile Menu Button */}
          <button
            onClick={() => setIsOpen(!isOpen)}
            className="md:hidden flex flex-col space-y-1.5"
            aria-label="Toggle menu"
            aria-expanded={isOpen}
          >
            <span className={`block w-6 h-0.5 transition-all duration-300 origin-center ${
              isScrolled ? 'bg-gray-800' : 'bg-white'
            } ${isOpen ? 'rotate-45 translate-y-2' : ''}`} />
            <span className={`block w-6 h-0.5 transition-all duration-300 ${
              isScrolled ? 'bg-gray-800' : 'bg-white'
            } ${isOpen ? 'opacity-0' : ''}`} />
            <span className={`block w-6 h-0.5 transition-all duration-300 origin-center ${
              isScrolled ? 'bg-gray-800' : 'bg-white'
            } ${isOpen ? '-rotate-45 -translate-y-2' : ''}`} />
          </button>
        </div>

        {/* Mobile Menu */}
        <div className={`md:hidden overflow-hidden transition-all duration-300 ease-in-out ${
          isOpen ? 'max-h-96' : 'max-h-0'
        }`}>
          <div className="pb-4 bg-white rounded-b-lg">
            {navLinks.map((link) => (
              <div key={link.label}>
                <a
                  href={link.href}
                  className="block py-2 px-4 text-gray-600 hover:bg-gray-100"
                >
                  {link.label}
                </a>
                {link.children?.map((child) => (
                  <a
                    key={child.label}
                    href={child.href}
                    className="block py-2 px-8 text-gray-500 hover:bg-gray-100"
                  >
                    {child.label}
                  </a>
                ))}
              </div>
            ))}
          </div>
        </div>
      </div>
    </nav>
  );
}

Accessibility Considerations

A proper Tailwind CSS navbar must be accessible to all users. Follow these guidelines:

Semantic HTML

<nav aria-label="Main navigation">
  <!-- navigation content -->
</nav>

Keyboard Navigation

Ensure all interactive elements are focusable:

<a href="#" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded">
  Home
</a>

ARIA Attributes

<!-- Mobile menu button -->
<button 
  id="menu-btn" 
  aria-label="Toggle navigation menu"
  aria-expanded="false"
  aria-controls="mobile-menu"
  class="md:hidden"
>
  <!-- hamburger icon -->
</button>

<!-- Mobile menu -->
<div id="mobile-menu" aria-hidden="true" class="hidden">
  <!-- menu items -->
</div>

<script>
  menuBtn.addEventListener('click', () => {
    const isExpanded = menuBtn.getAttribute('aria-expanded') === 'true';
    menuBtn.setAttribute('aria-expanded', !isExpanded);
    mobileMenu.setAttribute('aria-hidden', isExpanded);
    mobileMenu.classList.toggle('hidden');
  });
</script>

Add a skip link for keyboard users:

<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-white px-4 py-2 z-50">
  Skip to main content
</a>

<nav><!-- navbar --></nav>

<main id="main-content">
  <!-- page content -->
</main>

Focus Trap for Mobile Menu

When the mobile menu is open, trap focus within it:

const mobileMenu = document.getElementById('mobile-menu');
const focusableElements = mobileMenu.querySelectorAll('a, button');
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

mobileMenu.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === firstElement) {
      e.preventDefault();
      lastElement.focus();
    } else if (!e.shiftKey && document.activeElement === lastElement) {
      e.preventDefault();
      firstElement.focus();
    }
  }
  
  if (e.key === 'Escape') {
    // Close menu
    menuBtn.click();
    menuBtn.focus();
  }
});

Complete Example

Here’s a production-ready navbar combining all features:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Navbar</title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
  <!-- Skip Link -->
  <a href="#main" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-blue-600 text-white px-4 py-2 rounded z-[60]">
    Skip to main content
  </a>

  <nav id="navbar" class="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-gray-900" aria-label="Main navigation">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
      <div class="flex justify-between items-center h-16">
        <!-- Logo -->
        <a href="/" class="text-2xl font-bold text-white">
          Brand
        </a>

        <!-- Desktop Navigation -->
        <div class="hidden md:flex items-center space-x-8">
          <a href="/" class="text-gray-300 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 rounded">
            Home
          </a>
          
          <!-- Dropdown -->
          <div class="relative group">
            <button class="text-gray-300 hover:text-white transition-colors flex items-center focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 rounded">
              Services
              <svg class="w-4 h-4 ml-1 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" />
              </svg>
            </button>
            <div class="absolute left-0 mt-2 w-48 bg-white rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 transform group-hover:translate-y-0 translate-y-2">
              <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-t-lg">Web Design</a>
              <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-gray-100">Development</a>
              <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-gray-100">SEO</a>
              <a href="#" class="block px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-b-lg">Marketing</a>
            </div>
          </div>

          <a href="/about" class="text-gray-300 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 rounded">
            About
          </a>
          <a href="/contact" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900">
            Contact
          </a>
        </div>

        <!-- Mobile Menu Button -->
        <button 
          id="menu-btn" 
          class="md:hidden p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-white"
          aria-label="Toggle navigation menu"
          aria-expanded="false"
          aria-controls="mobile-menu"
        >
          <span id="line1" class="block w-6 h-0.5 bg-white transition-all duration-300 origin-center"></span>
          <span id="line2" class="block w-6 h-0.5 bg-white transition-all duration-300 mt-1.5"></span>
          <span id="line3" class="block w-6 h-0.5 bg-white transition-all duration-300 mt-1.5 origin-center"></span>
        </button>
      </div>

      <!-- Mobile Menu -->
      <div id="mobile-menu" class="md:hidden overflow-hidden max-h-0 transition-all duration-300" aria-hidden="true">
        <div class="py-4 space-y-2">
          <a href="/" class="block py-2 px-4 text-gray-300 hover:text-white hover:bg-gray-800 rounded-lg">Home</a>
          <a href="#" class="block py-2 px-4 text-gray-300 hover:text-white hover:bg-gray-800 rounded-lg">Services</a>
          <a href="/about" class="block py-2 px-4 text-gray-300 hover:text-white hover:bg-gray-800 rounded-lg">About</a>
          <a href="/contact" class="block py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-center">Contact</a>
        </div>
      </div>
    </div>
  </nav>

  <main id="main" class="pt-16">
    <!-- Hero Section for demo -->
    <div class="h-screen bg-gradient-to-br from-blue-600 to-purple-700 flex items-center justify-center">
      <h1 class="text-4xl md:text-6xl font-bold text-white text-center">
        Responsive Navbar Demo
      </h1>
    </div>
    <div class="h-screen bg-gray-100 flex items-center justify-center">
      <p class="text-xl text-gray-600">Scroll to see navbar effect</p>
    </div>
  </main>

  <script>
    const menuBtn = document.getElementById('menu-btn');
    const mobileMenu = document.getElementById('mobile-menu');
    const line1 = document.getElementById('line1');
    const line2 = document.getElementById('line2');
    const line3 = document.getElementById('line3');
    const navbar = document.getElementById('navbar');
    let isOpen = false;

    // Mobile menu toggle
    menuBtn.addEventListener('click', () => {
      isOpen = !isOpen;
      menuBtn.setAttribute('aria-expanded', isOpen);
      mobileMenu.setAttribute('aria-hidden', !isOpen);
      
      if (isOpen) {
        mobileMenu.style.maxHeight = mobileMenu.scrollHeight + 'px';
        line1.classList.add('rotate-45', 'translate-y-2');
        line2.classList.add('opacity-0');
        line3.classList.add('-rotate-45', '-translate-y-2');
      } else {
        mobileMenu.style.maxHeight = null;
        line1.classList.remove('rotate-45', 'translate-y-2');
        line2.classList.remove('opacity-0');
        line3.classList.remove('-rotate-45', '-translate-y-2');
      }
    });

    // Scroll effect
    window.addEventListener('scroll', () => {
      if (window.scrollY > 50) {
        navbar.classList.remove('bg-gray-900');
        navbar.classList.add('bg-white', 'shadow-lg');
        document.querySelectorAll('#navbar a:not(.bg-blue-600)').forEach(link => {
          link.classList.remove('text-gray-300', 'text-white');
          link.classList.add('text-gray-600', 'hover:text-gray-900');
        });
        [line1, line2, line3].forEach(line => {
          line.classList.remove('bg-white');
          line.classList.add('bg-gray-800');
        });
      } else {
        navbar.classList.add('bg-gray-900');
        navbar.classList.remove('bg-white', 'shadow-lg');
        document.querySelectorAll('#navbar a:not(.bg-blue-600)').forEach(link => {
          link.classList.add('text-gray-300');
          link.classList.remove('text-gray-600', 'hover:text-gray-900');
        });
        [line1, line2, line3].forEach(line => {
          line.classList.add('bg-white');
          line.classList.remove('bg-gray-800');
        });
      }
    });

    // Close menu on escape
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && isOpen) {
        menuBtn.click();
      }
    });
  </script>
</body>
</html>

Conclusion

Building a responsive Tailwind CSS navbar involves combining utility classes for layout, responsive design, and interactivity. The key takeaways:

  1. Use hidden md:flex to toggle between mobile and desktop layouts
  2. Leverage Tailwind’s group and group-hover for CSS-only dropdowns
  3. Add sticky top-0 or fixed top-0 for scroll-persistent navigation
  4. Implement smooth animations with transition-all duration-300
  5. Always include proper ARIA attributes for accessibility

With these patterns, you can create professional navigation bars that work flawlessly across all devices and screen sizes.