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 scrollz-50: Stays above other contentbg-white/80: 80% opacity white backgroundbackdrop-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-expandedon the toggle button -
aria-controlslinking button to menu -
aria-labelon 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.