oward

NavLink

Navigation link wrapper component that detects and highlights active state based on the current URL.

beta

Installation

npx oward-ui add navlink

Introduction

In most React applications, managing the active state of navigation links requires using hooks like usePathname() or useLocation() in each component, then writing URL comparison logic to apply the appropriate styles. This quickly becomes repetitive and hard to maintain.

NavLink solves this problem by centralizing all this logic into a reusable component. It automatically detects if the link matches the current URL and applies the appropriate CSS classes, without you having to write hooks or conditional logic.

Detection happens client-side after rendering, which allows it to work correctly with components like Link from next-intl or any other system that rewrites URLs. The component analyzes the final URL rendered in the DOM, ensuring accurate matching regardless of the routing library used.

This significantly improves the developer experience while ensuring consistent navigation behavior throughout your application.

Features

  • SSR and SSG compatible: Works perfectly with server-side rendering and static site generation.
  • Supports all routing systems: Compatible with Next.js, React Router, Next-intl...
  • Flexible matching modes: "auto", "exact" or boolean for full control.
  • Advanced options: Include hash and query parameters in matching.
  • Built-in accessibility: Automatically adds aria-current="page" for better accessibility.
  • Performance optimized: Uses the Navigation API

Usage

The NavLink component wraps any link element (Next.js Link, React Router, etc.) and automatically applies a CSS class when the URL matches the link.

Basic example

import { NavLink } from "@/ui/navlink";
import Link from "next/link";

export default function Navigation() {
  return (
    <nav className="flex gap-4">
      <NavLink currentClassName="text-blue-600 font-semibold">
        <Link href="/about">About</Link>
      </NavLink>
      <NavLink currentClassName="text-blue-600 font-semibold">
        <Link href="/contact">Contact</Link>
      </NavLink>
    </nav>
  );
}

Props

PropTypeDefaultDescription
current"auto" | "exact" | boolean"auto"URL matching mode
currentClassNamestring-CSS class to apply when the link is active
patternstring-URL pattern with wildcards for matching
matchHashbooleanfalseInclude hash in URL matching
matchQuerybooleanfalseInclude query parameters in URL matching
classNamestring-Additional CSS classes to apply

Matching modes

"auto" mode (default)

Matches if the current URL starts with the link's href.

<NavLink current="auto" currentClassName="text-blue-600">
  <Link href="/docs">Documentation</Link>
</NavLink>
Current URLLink hrefActive?
/docs/docs✅ Yes
/docs/installation/docs✅ Yes
/documentation/docs❌ No
//✅ Yes
/home/✅ Yes
/about/✅ Yes

"exact" mode

Matches only if the current URL is exactly identical to the link's href.

<NavLink current="exact" currentClassName="text-blue-600">
  <Link href="/docs">Documentation</Link>
</NavLink>
Current URLLink hrefActive?
/docs/docs✅ Yes
/docs/installation/docs❌ No
/docs?tab=api/docs❌ No

Boolean mode

Manually forces the active/inactive state, ignoring the URL.

<NavLink current={isActive} currentClassName="text-blue-600">
  <Link href="/profile">Profile</Link>
</NavLink>

Pattern with wildcards

The pattern prop allows you to define a URL pattern with wildcards for matching. This is mainly useful when you need to use NavLink with elements without href (like buttons with onClick) while keeping URL-based active state detection.

Why pattern?

For regular links (with href), NavLink automatically compares the URL rendered in the DOM after render. This works perfectly with libraries like next-intl that add a locale prefix. The pattern is therefore reserved for cases where the child element has no href.

Wildcard syntax

  • * = exactly one segment (anything except /)
PatternURLMatches?
/*/dashboard/fr/dashboard✅ Yes
/*/dashboard/en/dashboard✅ Yes
/*/dashboard/dashboard❌ No
/*/dashboard/*/fr/dashboard/settings✅ Yes
/*/dashboard/*/en/dashboard/account✅ Yes

Example with a button

import { NavLink } from "@/ui/navlink";
import { useRouter } from "next/navigation";

export function DashboardNav() {
  const router = useRouter();

  return (
    <nav className="flex gap-4">
      {/* Button that navigates programmatically */}
      <NavLink pattern="/*/dashboard" currentClassName="bg-primary text-white">
        <button onClick={() => router.push("/dashboard")}>
          Dashboard
        </button>
      </NavLink>
      <NavLink pattern="/*/settings" currentClassName="bg-primary text-white">
        <button onClick={() => router.push("/settings")}>
          Settings
        </button>
      </NavLink>
    </nav>
  );
}

Interaction with current

The pattern respects the matching mode defined by current:

  • current="auto" (default): pattern matches if the URL starts with the pattern
  • current="exact": pattern matches if the URL exactly matches the pattern
{/* Auto mode: /fr/dashboard/users will match */}
<NavLink pattern="/*/dashboard" currentClassName="active">
  <Link href="/dashboard">Dashboard</Link>
</NavLink>

{/* Exact mode: only /fr/dashboard will match */}
<NavLink pattern="/*/dashboard" current="exact" currentClassName="active">
  <Link href="/dashboard">Dashboard</Link>
</NavLink>
Current URLPatterncurrent="auto"current="exact"
/fr/dashboard/*/dashboard✅ Active✅ Active
/fr/dashboard/users/*/dashboard✅ Active❌ Inactive

Pattern vs href

When pattern is defined, it is used for matching instead of the child element's href. This allows using NavLink with any React element, not just links.

{/* Usage with a button */}
<NavLink pattern="/*/dashboard" currentClassName="bg-primary text-white">
  <button onClick={handleClick}>Dashboard</button>
</NavLink>

Advanced options

Include hash in matching

Use case

By default, the hash is ignored. Enable this option if you want each anchor to be considered as a distinct route.

<NavLink matchHash currentClassName="font-bold">
  <Link href="/docs#api">Documentation</Link>
</NavLink>
Current URLLink hrefDefault (matchHash=false)matchHash={true}
/docs#api/docs#api✅ Active✅ Active
/docs#intro/docs#api✅ Active❌ Inactive

Include query parameters in matching

Use case

By default, query parameters are ignored. Enable this option if you want each parameter combination to be considered as a distinct route.

<NavLink matchQuery currentClassName="font-bold">
  <Link href="/products">Products</Link>
</NavLink>
Current URLLink hrefDefault (matchQuery=false)matchQuery={true}
/products/products✅ Active✅ Active
/products?category=shoes/products✅ Active❌ Inactive

Accessibility

The component automatically adds the aria-current="page" attribute when the link is active, following WCAG 2.1 standards.

// HTML output when active
<a href="/about" aria-current="page" className="text-blue-600">
  About
</a>

Performance

Performance optimizations

The component uses a global location tracking system shared between all NavLink instances to minimize re-renders:

  • On modern browsers, it uses the Navigation API.
  • On older browsers, a lightweight polling (250ms) is used as a fallback. This fallback will be removed in a future version.

Practical examples

Composition with asChild

NavLink is compatible with Radix UI's asChild pattern. You can wrap it in components like Button, DropdownMenuItem, or any other component supporting asChild to combine their styles and behaviors.

import { NavLink } from "@/ui/navlink";
import { Button } from "@/ui/button";
import Link from "next/link";

export function MainNav() {
  return (
    <nav className="flex gap-6">
      <Button asChild variant="ghost">
        <NavLink currentClassName="text-primary font-semibold border-b-2 border-primary">
          <Link href="/">Home</Link>
        </NavLink>
      </Button>
      <Button asChild variant="ghost">
        <NavLink currentClassName="text-primary font-semibold border-b-2 border-primary">
          <Link href="/contact">Contact</Link>
        </NavLink>
      </Button>
    </nav>
  );
}

Limitation

NavLink's child component must be a valid React element with an href prop. In development mode, an error will be thrown if this condition is not met.

On this page