NavLink
Navigation link wrapper component that detects and highlights active state based on the current URL.
Installation
npx oward-ui add navlinkIntroduction
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
| Prop | Type | Default | Description |
|---|---|---|---|
current | "auto" | "exact" | boolean | "auto" | URL matching mode |
currentClassName | string | - | CSS class to apply when the link is active |
pattern | string | - | URL pattern with wildcards for matching |
matchHash | boolean | false | Include hash in URL matching |
matchQuery | boolean | false | Include query parameters in URL matching |
className | string | - | 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 URL | Link href | Active? |
|---|---|---|
/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 URL | Link href | Active? |
|---|---|---|
/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/)
| Pattern | URL | Matches? |
|---|---|---|
/*/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 patterncurrent="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 URL | Pattern | current="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 URL | Link href | Default (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 URL | Link href | Default (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.