Simplifying Active Navigation in Next.js with a Context-Based RouterLink

Introduction

Building user-friendly and accessible navigation is a cornerstone of modern web development. In applications using frameworks like Next.js, it's crucial to indicate which navigation link corresponds to the current page. This ensures a clear and intuitive experience for users while improving accessibility for assistive technologies and search engine optimization (SEO). In this post, we'll explore how to create a reusable, scalable RouterLink component in Next.js, leveraging React Context to handle active link states effectively.

Why You Need Active Navigation Indication

Active link indication serves two critical purposes:

  1. User Experience: It provides visual feedback, such as highlighting the active link, to guide users within your application.
  2. Accessibility (WCAG Compliance): According to the Web Content Accessibility Guidelines (WCAG) Success Criterion 2.4.7, it is essential to provide clear indications of the user's current location within a website or application. Using the aria-current attribute is a recommended practice to meet this criterion, enhancing accessibility for users relying on screen readers.
  3. Search Engine Optimization (SEO): Clear and structured navigation improves crawlability, making it easier for search engines to index your site and understand its structure, boosting your site's SEO performance.

While you can manually manage active states using tools like useRouter or usePathname, this approach can lead to repetitive code, especially in large applications with multiple navigation links. A context-based solution not only removes redundancy but also ensures cleaner, more maintainable code.

Challenges with Manual Active State Management

In Next.js, developers often rely on the useRouter or usePathname hooks to determine the current route. For instance:

1
import Link from "next/link"
2
import { useRouter } from "next/router"
3
4
export const RouterLink = ({
5
href,
6
children,
7
}: {
8
href: string
9
children: React.ReactNode
10
}) => {
11
const { asPath } = useRouter()
12
13
const isActive = asPath === href
14
const ariaCurrent = isActive ? "page" : undefined
15
16
return (
17
<Link href={href} aria-current={ariaCurrent}>
18
{children}
19
</Link>
20
)
21
}

While functional, this approach becomes cumbersome when:

  • Repeatedly comparing asPath or usePathname in every link component.
  • Passing props like currentPath manually across deeply nested components.

By introducing a RouterLinkProvider with React Context, you can streamline this process and centralize route management.

The Context-Based Solution

The goal is to create a RouterLinkProvider that automatically provides the currentPath (using usePathname) to all RouterLink components. This eliminates the need for redundant logic and manual prop passing.

Step 1: Setting Up the Context

Create a RouterLinkContext using React's createContext API:

1
"use client"
2
3
import { createContext, ReactNode, useContext } from "react"
4
import { usePathname } from "next/navigation"
5
6
const RouterLinkContext = createContext<string | undefined>(undefined)
7
8
export const RouterLinkProvider = ({ children }: { children: ReactNode }) => {
9
const pathname = usePathname() // Automatically gets the current path
10
11
return (
12
<RouterLinkContext.Provider value={pathname}>
13
{children}
14
</RouterLinkContext.Provider>
15
)
16
}
17
18
export const useRouterLinkContext = () => {
19
const context = useContext(RouterLinkContext)
20
if (!context) {
21
throw new Error(
22
"useRouterLinkContext must be used within a RouterLinkProvider"
23
)
24
}
25
return context
26
}

This provider ensures that the pathname is accessible to all components within the context.

Refactor the RouterLink component to consume the currentPath from the context:

1
import { AnchorHTMLAttributes, ReactNode } from "react"
2
import Link, { LinkProps } from "next/link"
3
4
import { useRouterLinkContext } from "./RouterLinkContext"
5
6
type RouterLinkProps = LinkProps &
7
AnchorHTMLAttributes<HTMLAnchorElement> & {
8
children: ReactNode
9
}
10
11
export const RouterLink = ({ href, children, ...props }: RouterLinkProps) => {
12
const currentPath = useRouterLinkContext() // Access current path from context
13
14
const isActive = currentPath === href
15
const ariaCurrent = isActive ? "page" : undefined
16
17
return (
18
<Link {...props} href={href} aria-current={ariaCurrent}>
19
{children}
20
</Link>
21
)
22
}

This component dynamically calculates the active state and adds the appropriate aria-current attribute.

Step 3: Integrating the Provider

Wrap your application with the RouterLinkProvider in your root layout or a higher-level component:

1
import { RouterLinkProvider } from "./RouterLinkContext"
2
3
export default function RootLayout({
4
children,
5
}: {
6
children: React.ReactNode
7
}) {
8
return (
9
<html>
10
<body>
11
<RouterLinkProvider>{children}</RouterLinkProvider>
12
</body>
13
</html>
14
)
15
}

Now you can use the RouterLink component without worrying about manually managing the active state:

1
import { RouterLink } from "./RouterLink"
2
3
export default function Navigation() {
4
return (
5
<nav>
6
<RouterLink href="/about" className="text-gray-500">
7
About Us
8
</RouterLink>
9
<RouterLink href="/contact" className="text-gray-500">
10
Contact Us
11
</RouterLink>
12
</nav>
13
)
14
}

Benefits of the Context-Based Approach

  1. Centralized State Management: The RouterLinkProvider handles the current path globally, reducing redundancy.
  2. Improved Readability: No need to pass currentPath manually to each RouterLink.
  3. Scalability: Works seamlessly in large applications with deeply nested navigation components.
  4. Accessibility: Automatically adds aria-current for active links, meeting WCAG Success Criterion 2.4.7.
  5. Enhanced SEO: Clear navigation with aria-current helps search engines understand the site's structure, improving indexation and ranking.

Conclusion

Building accessible and intuitive navigation doesn't have to be repetitive or cumbersome. By leveraging React Context and Next.js's usePathname, you can create a scalable and reusable RouterLink system that simplifies your codebase and improves the user experience. This approach is particularly beneficial for large applications where manual active state management can quickly become unmanageable.

With the RouterLinkProvider in place, your navigation logic is centralized, your components are cleaner, and your users—both human and machine—will thank you for the seamless experience. Start implementing it today and make your Next.js apps more robust, accessible, and SEO-friendly!