How to Style Active Links in Next.js App Router

Nearly all websites have some form of navigation and a common pattern is styling the active link, or the link that represents the current page. This is typically simple with easy access to the current path, but how does that work with the Next.js App Router?

Table of Contents

YouTube Preview
View on YouTube

Tip: We step through a bit of context before landing on the solution. If you’re looking to get in and out, jump down to: Getting a Page’s Current Path with usePathname

Adding Navigation to Next.js Layouts

Let’s start off by adding an example of how we want to approach this.

A common location for storing a navigation, whether at the top of the page or as a sidebar, is inside of a Next.js layout.js file.

Layouts are a common pattern of the web and Next.js took this concept even further in the App Router by making Layouts “special” including being able to develop nested Layouts.

While we’re not going to dive too deep into those capabilities, the important part is we’ll be able to use the layout.jsx file (or layout.tsx) to embed our navigation.

To do this, if you haven’t already, create a layout.jsx or layout.tsx file in the root of your project’s app directory or inside of a route segment (a folder).

A simple layout may look like this inside of app/layout.tsx:

import Link from 'next/link';

export default async function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <aside>
        <ul>
          <li>
            <Link href="/page-1">Page 1</Link>
          </li>
          <li>
            <Link href="/page-2">Page 2</Link>
          </li>
        </ul>
      </aside>
      <div>{ children }</div>
    </div>
  );
}

Here we’re expecting there are to be two pages available, Page 1 and Page 2, at the respective paths.

In my example, I’m using a sidebar that includes Movies and TV Shows links.

Page with movie thumbs and sidebar links

Now our goal would ultimately be to style these two differently, whether we would change the color of the link, the background of the link, or maybe a some kind of symbol, but how can we find out which link is the active link?

Getting the Current Path in a Next.js Layout

Before getting the current path, we have to understand the environment we’re working in.

In Next.js, we may be working in a server or client component. Let’s assume at this point we’re working inside of a server component.

But the tricky part is by default, we don’t actually have the ability to simply access the page path from within a server component.

This might seem odd, as it’s such a common thing to need to access, but let’s first use a workaround to get the page path on the server, where we can then gain some insight into potentially why this is the case.

Using Next.js Middleware to Access the Current Page’s Path on the Server

While we don’t have direct access to the page’s path from within our server component, there is a mechanism that gives us access to that path upon each request, and that’s inside Next.js middleware.

Once we obtain that path, we can then go ahead and store that inside of the headers, where we can then grab it from within our component.

Starting off, let’s set up our middleware.

In the root of your project (adjacent to app) add middleware.tsx.

Inside middleware.tsx add:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export default function middleware(request: NextRequest) {
  const requestHeaders = new Headers(request.headers);

  requestHeaders.set('x-next-pathname', request.nextUrl.pathname);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Thanks Lajos for this example.

Every time a page request occurs (or requests that match the config), we set a custom header of x-next-path to the value of the current request’s pathname.

Inside of our Layout file, we can now try to access that.

First import the headers module inside of layout.tsx with:

import { headers } from 'next/headers';

Then let’s try to log out our pathname.

Inside of your layout.tsx file add:

const pathname = headers().get('x-next-pathname');
console.log('pathname', pathname);

And if we refresh the page and look in the terminal, we should see the current page path.

Page path logged in terminal

But wait, there’s a problem here.

Try navigating to one of the other links.

If we try looking in the terminal, we’re not actually going to see another logged instance of pathname.

Terminal showing only 1 request logged with pathname

This is where we have a problem with trying to access the pathname on the server.

Our Layout only renders once on the server. When we navigate around to other pages within our layout, that’s all getting managed from the client without our Layout having to rerender.

Vercel themselves recommends not doing this for that exact reason.

So how can we handle this?

Getting a Page’s Current Path with usePathname

The good news is that Next.js has a solution, though it comes with a caveat.

We have access to the usePathname hook which gives us exactly what we need.

But we’ll need to opt in our component hat uses usePathname as a client component (as well as the component tree) to use it.

Note: Opting into client components “is not a bad thing” but it comes with potential implications, especially considering opting into an entire layout and its tree as a client component. But we’ll see how we can address this.

First, let’s add the "use client"; directive to the top of our layout.tsx file.

Next import usePathname with:

import { usePathname } from 'next/navigation'

With it imported, we can now access the pathname at the top of our Layout:

const pathname = usePathname();

And finally, we can now update our links so that we can dynamically style the links differently based on the current path.

For instance, if we were using Tailwind, it might look like:

<li>
  <Link className={pathname === '/page-1' ? 'color-white bg-blue-500' : ''} href="/page-1">
    Page 1
  </Link>
</li>

pathname will reflect the current path on first load and whenever we change pages, so when we’re on the page defined, we’ll get those styles applied.

Active link styled differently in sidebar

But this doesn’t feel right, we just opted in our entire tree under layout.tsx as a client tree, can we avoid that?

Yes! We can abstract our sidebar into a new component, opt that component into a client tree, and get the best of both worlds.

First off, let’s remove the "use client"; directive and usePathname import from layout.tsx.

Next, let’s create a new component wherever you prefer to create your components. I like to maintain mine inside of src/components.

Create a new file Sidebar.tsx and inside add:

"use client";

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

export default function Sidebar({ links }: { links: Array<Record<string, string>>}) {
  const pathname = usePathname();
  return (
    <ul>
      {links.map(link => {
        const isActive = pathname === link.path;
        return (
          <li key={`${link.label}-${link.path}`}>
            <Link className={isActive ? 'color-white bg-blue-500' : ''} href={link.path}>
              { link.label }
            </Link>
          </li>
        )
      })}
    </ul>
  )
}

Here we’re creating a component that takes in a prop of links which will be an array of objects including a path and a label.

Those links are mapped out, creating a new link list item in an unordered list.

We’re using the usePathname hook like we did earlier to access the pathname and determine if our path is active. If it is, we apply the same styles as before.

Note that this is a client component as we have our "use client"; directive at the top.

Back inside of our layout.tsx file, we can now import that component:

import Sidebar from '@/components/Sidebar';

And use it to render our links:

<Sidebar
  links={[
    {
      label: 'Page 1',
      path: '/page-1',
    },
    {
      label: 'Page 2',
      path: '/page-2',
    },
  ]}
/>

And when we refresh our page, we should still see our active link! But this time, only our Sidebar component has opted into a client component, leaving the rest of our Layout tree’s branches in tact as server components (assuming they were to begin with!).

Tip: You could also use the “donut” pattern depending on your use case like this example from the Vercel team.

Figuring out how to navigate between client and server components in some ways feels simple yet complex in others, but as time goes on we’ll see more patterns emerge helping us to rethink the challenges we’re trying to solve.

Thanks for everyone who chimed in on Twitter to help get to this solution!