Skip to main content
Back to Blog
Tutorials
3 min read
November 14, 2024

How to Build a Responsive Navigation Bar with Tailwind CSS

Build a responsive navigation bar with mobile hamburger menu using Tailwind CSS. No JavaScript library required β€” just React state and Tailwind utilities.

Ryel Banfield

Founder & Lead Developer

A navigation bar needs to look good on desktop and collapse into a mobile menu on small screens. Here is how to build one with Tailwind CSS and React β€” no external navigation library needed.

The Finished Result

  • Desktop: Horizontal navigation links with logo
  • Mobile: Hamburger icon that toggles a full-screen menu
  • Accessible: Proper ARIA attributes and keyboard navigation
  • Animated: Smooth transitions for mobile menu

Step 1: Basic Structure

// components/layout/Navbar.tsx
"use client";

import Link from "next/link";
import { useState } from "react";

const navLinks = [
  { href: "/services", label: "Services" },
  { href: "/about", label: "About" },
  { href: "/blog", label: "Blog" },
  { href: "/contact", label: "Contact" },
];

export function Navbar() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <header className="sticky top-0 z-50 border-b bg-white/80 backdrop-blur-md dark:bg-gray-950/80">
      <nav className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
        {/* Logo */}
        <Link href="/" className="text-xl font-bold">
          YourBrand
        </Link>

        {/* Desktop Navigation */}
        <ul className="hidden items-center gap-8 md:flex">
          {navLinks.map((link) => (
            <li key={link.href}>
              <Link
                href={link.href}
                className="text-sm font-medium text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
              >
                {link.label}
              </Link>
            </li>
          ))}
          <li>
            <Link
              href="/contact"
              className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
            >
              Get a Quote
            </Link>
          </li>
        </ul>

        {/* Mobile Menu Button */}
        <button
          onClick={() => setIsOpen(!isOpen)}
          className="md:hidden"
          aria-expanded={isOpen}
          aria-controls="mobile-menu"
          aria-label={isOpen ? "Close menu" : "Open menu"}
        >
          <span className="sr-only">{isOpen ? "Close menu" : "Open menu"}</span>
          {isOpen ? <XIcon /> : <MenuIcon />}
        </button>
      </nav>

      {/* Mobile Menu */}
      {isOpen && (
        <div
          id="mobile-menu"
          className="border-t md:hidden"
          role="navigation"
          aria-label="Mobile navigation"
        >
          <ul className="space-y-1 px-4 py-4">
            {navLinks.map((link) => (
              <li key={link.href}>
                <Link
                  href={link.href}
                  onClick={() => setIsOpen(false)}
                  className="block rounded-md px-3 py-2 text-base font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
                >
                  {link.label}
                </Link>
              </li>
            ))}
            <li className="pt-2">
              <Link
                href="/contact"
                onClick={() => setIsOpen(false)}
                className="block rounded-md bg-blue-600 px-3 py-2 text-center text-base font-medium text-white"
              >
                Get a Quote
              </Link>
            </li>
          </ul>
        </div>
      )}
    </header>
  );
}

function MenuIcon() {
  return (
    <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
    </svg>
  );
}

function XIcon() {
  return (
    <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
    </svg>
  );
}

Step 2: Sticky Header with Backdrop Blur

The key classes for a modern sticky header:

sticky top-0 z-50 border-b bg-white/80 backdrop-blur-md
  • sticky top-0: Sticks to the top on scroll
  • z-50: Stays above other content
  • bg-white/80: 80% opacity white background
  • backdrop-blur-md: Blurs content behind the header

This creates the glassmorphism effect popular in modern web design.

Step 3: Add Animation to Mobile Menu

Replace the conditional render with an animated version:

<div
  id="mobile-menu"
  className={`overflow-hidden border-t transition-all duration-300 ease-in-out md:hidden ${
    isOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
  }`}
>
  {/* Menu content */}
</div>

Always render the menu in the DOM but use max-h-0 / max-h-96 with overflow-hidden and transition-all for a smooth slide animation.

Step 4: Close on Route Change

Close the mobile menu when the user navigates:

import { usePathname } from "next/navigation";
import { useEffect } from "react";

export function Navbar() {
  const [isOpen, setIsOpen] = useState(false);
  const pathname = usePathname();

  // Close menu on route change
  useEffect(() => {
    setIsOpen(false);
  }, [pathname]);

  // ...
}

Step 5: Active Link Styling

Highlight the current page in the navigation:

import { usePathname } from "next/navigation";

function NavLink({ href, label }: { href: string; label: string }) {
  const pathname = usePathname();
  const isActive = pathname === href || pathname.startsWith(`${href}/`);

  return (
    <Link
      href={href}
      className={`text-sm font-medium transition-colors ${
        isActive
          ? "text-blue-600 dark:text-blue-400"
          : "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
      }`}
      aria-current={isActive ? "page" : undefined}
    >
      {label}
    </Link>
  );
}

Step 6: Close on Escape Key

useEffect(() => {
  function handleEscape(event: KeyboardEvent) {
    if (event.key === "Escape") {
      setIsOpen(false);
    }
  }

  document.addEventListener("keydown", handleEscape);
  return () => document.removeEventListener("keydown", handleEscape);
}, []);

Accessibility Checklist

  • aria-expanded on the toggle button
  • aria-controls linking button to menu
  • aria-label on the toggle button
  • role="navigation" on the mobile menu
  • aria-current="page" on the active link
  • Keyboard-accessible (Tab, Escape)
  • Focusable links in the mobile menu

Responsive Breakpoint

The md: breakpoint (768px) is used as the switch point:

  • Below 768px: mobile menu with hamburger
  • 768px and above: horizontal desktop navigation

Adjust to lg: (1024px) if you have many navigation items.

Need a Professional Navigation?

We design and build custom navigation systems including mega menus, search integration, and multi-level dropdowns. Contact us for your website project.

Tailwind CSSnavbarresponsivenavigationtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles