Published on

Active Menu Highlighting in Next.js with usePathname

Authors
  • Mehdi Akiki avatar
    Name
    Mehdi Akiki
    Twitter

Active Menu Highlighting in Next.js with usePathname

One of those small UX details that makes your site feel professional: highlighting the current page in your navigation menu. Here's how to do it in Next.js 14+ with App Router.

Why It Matters

Active menu highlighting tells users where they are. Without it, your navigation feels disconnected. With it, your site feels polished and intuitive.

The Hook: usePathname

Next.js provides usePathname() from next/navigation. It returns the current URL path as a string.

import { usePathname } from "next/navigation";

const pathname = usePathname();
// On /blog → pathname = "/blog"
// On /blog/my-post → pathname = "/blog/my-post"

Basic Implementation

Here's the simplest version for a desktop header:

"use client";

import { usePathname } from "next/navigation";
import Link from "next/link";

const navLinks = [
  { href: "/blog", title: "Blog" },
  { href: "/work", title: "Work" },
  { href: "/contact", title: "Contact" },
];

export default function Header() {
  const pathname = usePathname();

  return (
    <nav>
      {navLinks.map((link) => {
        const isActive = pathname === link.href;

        return (
          <Link
            key={link.href}
            href={link.href}
            className={`
              ${
                isActive
                  ? "border-b-2 border-blue-600 text-blue-600"
                  : "text-gray-900 hover:text-blue-500"
              }
            `}
          >
            {link.title}
          </Link>
        );
      })}
    </nav>
  );
}

Handling Nested Routes

Problem: /blog shouldn't just match /blog, it should also match /blog/my-post.

Solution: Use startsWith():

const isActive = pathname === link.href || pathname.startsWith(link.href + "/");

This activates /blog when you're on /blog/my-post but not when you're on /bloggers (note the trailing /).

Multiple Visual States

You might want different styles for:

  • Active page (primary color + underline)
  • Special CTA (green color, always stands out)
  • Normal links (gray, neutral)
const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
const isHire = link.href === "/hire";

return (
  <Link
    href={link.href}
    className={`
      ${
        isHire
          ? `font-semibold text-green-600
           ${isActive ? "border-b-2 border-green-600" : ""}`
          : isActive
            ? "border-b-2 border-blue-600 text-blue-600"
            : "text-gray-900 hover:text-blue-500"
      }
    `}
  >
    {link.title}
  </Link>
);

This keeps your CTA green (special) while still showing active state with an underline.

Mobile vs Desktop Styling

Desktop: Bottom border (underline) Mobile: Left border (vertical indicator)

// Desktop (Header.tsx)
className={isActive
  ? 'text-blue-600 border-b-2 border-blue-600'
  : 'text-gray-900'
}

// Mobile (MobileNav.tsx)
className={isActive
  ? 'text-blue-600 border-l-4 border-blue-600 pl-8'
  : 'text-gray-900'
}

The pl-8 (padding-left) on mobile compensates for the 4px border so text doesn't shift.

Important: Client Component

usePathname() only works in Client Components. Add "use client" at the top:

"use client";

import { usePathname } from "next/navigation";
// ... rest of your component

Complete Working Example

Here's what I use on this site:

"use client";

import { usePathname } from "next/navigation";
import Link from "./Link";
import headerNavLinks from "@/data/headerNavLinks";

export default function Header() {
  const pathname = usePathname();

  return (
    <header>
      <nav>
        {headerNavLinks
          .filter((link) => link.href !== "/")
          .map((link) => {
            const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
            const isHire = link.href === "/hire";

            return (
              <Link
                key={link.title}
                href={link.href}
                className={`block font-medium ${
                  isHire
                    ? `font-semibold text-green-600 hover:text-green-700
                       ${isActive ? "border-b-2 border-green-600" : ""}`
                    : isActive
                      ? "border-b-2 border-primary-500 text-primary-500"
                      : "text-gray-900 hover:text-primary-500"
                }`}
              >
                {link.title}
              </Link>
            );
          })}
      </nav>
    </header>
  );
}

Testing

Navigate between pages and check:

  • ✅ Active page shows highlight
  • ✅ Nested routes activate parent (e.g., /blog/post highlights "Blog")
  • ✅ Only one item highlighted at a time
  • ✅ Special CTAs maintain their color
  • ✅ Works on both mobile and desktop

Common Gotchas

"My component isn't updating!"

  • Did you add "use client" at the top?

"All my blog posts highlight 'Blog' in the menu"

  • Good! That's the startsWith() logic working correctly.

"usePathname is not a function"

  • Import from next/navigation, not next/router (old Pages Router).

"The highlight flashes on page load"

  • Normal with client components. You can use CSS transitions to smooth it:
    transition: border-color 200ms ease-in-out;
    

That's It

Five-minute implementation, professional-looking navigation. This is one of those small touches that makes your site feel complete.

For more Next.js patterns, check out the Next.js docs on usePathname.

I build and scale reliable production systems. Open to full-time and freelance work with U.S.-based teams that value ownership and execution.

Got something in mind?

Book a Discovery Call