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:
- User Experience: It provides visual feedback, such as highlighting the active link, to guide users within your application.
- 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. - 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:
1import Link from "next/link"2import { useRouter } from "next/router"34export const RouterLink = ({5href,6children,7}: {8href: string9children: React.ReactNode10}) => {11const { asPath } = useRouter()1213const isActive = asPath === href14const ariaCurrent = isActive ? "page" : undefined1516return (17<Link href={href} aria-current={ariaCurrent}>18{children}19</Link>20)21}
While functional, this approach becomes cumbersome when:
- Repeatedly comparing
asPath
orusePathname
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"23import { createContext, ReactNode, useContext } from "react"4import { usePathname } from "next/navigation"56const RouterLinkContext = createContext<string | undefined>(undefined)78export const RouterLinkProvider = ({ children }: { children: ReactNode }) => {9const pathname = usePathname() // Automatically gets the current path1011return (12<RouterLinkContext.Provider value={pathname}>13{children}14</RouterLinkContext.Provider>15)16}1718export const useRouterLinkContext = () => {19const context = useContext(RouterLinkContext)20if (!context) {21throw new Error(22"useRouterLinkContext must be used within a RouterLinkProvider"23)24}25return context26}
This provider ensures that the pathname
is accessible to all components within the context.
Step 2: Updating the RouterLink
Component
Refactor the RouterLink
component to consume the currentPath
from the context:
1import { AnchorHTMLAttributes, ReactNode } from "react"2import Link, { LinkProps } from "next/link"34import { useRouterLinkContext } from "./RouterLinkContext"56type RouterLinkProps = LinkProps &7AnchorHTMLAttributes<HTMLAnchorElement> & {8children: ReactNode9}1011export const RouterLink = ({ href, children, ...props }: RouterLinkProps) => {12const currentPath = useRouterLinkContext() // Access current path from context1314const isActive = currentPath === href15const ariaCurrent = isActive ? "page" : undefined1617return (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:
1import { RouterLinkProvider } from "./RouterLinkContext"23export default function RootLayout({4children,5}: {6children: React.ReactNode7}) {8return (9<html>10<body>11<RouterLinkProvider>{children}</RouterLinkProvider>12</body>13</html>14)15}
Step 4: Using RouterLink
Now you can use the RouterLink
component without worrying about manually managing the active state:
1import { RouterLink } from "./RouterLink"23export default function Navigation() {4return (5<nav>6<RouterLink href="/about" className="text-gray-500">7About Us8</RouterLink>9<RouterLink href="/contact" className="text-gray-500">10Contact Us11</RouterLink>12</nav>13)14}
Benefits of the Context-Based Approach
- Centralized State Management: The
RouterLinkProvider
handles the current path globally, reducing redundancy. - Improved Readability: No need to pass
currentPath
manually to eachRouterLink
. - Scalability: Works seamlessly in large applications with deeply nested navigation components.
- Accessibility: Automatically adds
aria-current
for active links, meeting WCAG Success Criterion 2.4.7. - 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!