Home/Blog/Next Js/How to Build an Active Link Indicator in Next.js - Navbar and Sidebar
How to Build an Active Link Indicator in Next.js - Navbar and Sidebar
Next Js9 min readAug 15, 2026

How to Build an Active Link Indicator in Next.js - Navbar and Sidebar

Learn how to build an active link indicator in Next.js using usePathname - works for both navbar and sidebar with clean Tailwind CSS styling.

115 views1 likes1 comments0 bookmarks

How to Build an Active Link Indicator in Next.js - Navbar and Sidebar

If you have built a navbar in Next.js, you have probably run into this question - how do I highlight the link for the page the user is currently on? The active link indicator is one of those small UI details that makes your app feel polished and tells the user exactly where they are.

The good news is Next.js makes this surprisingly simple with one hook - usePathname. This blog walks through the complete setup, from creating the navbar to adding the active indicator, including a sidebar version at the end.

If you are not yet familiar with how Next.js routing works, check out the SSR, CSR, SSG, and ISR blog on codekk.dev - it covers the App Router and how pages are structured in a Next.js project.


What We Are Building

A navbar with navigation links where the currently active route gets a visible indicator - in this case a blue bottom border. As you navigate between pages, the indicator moves to the correct link automatically.

At the end we will also build the same thing for a sidebar, using a left border instead of a bottom border.


Project Setup

Start with a fresh Next.js project if you do not have one:

Code
npx create-next-app@latest active-link-indicator

Choose TypeScript, Tailwind CSS, and the App Router when prompted.

Your folder structure will look like this:

Code
└── active-link-indicator
    └── app
        ├── favicon.ico
        ├── globals.css
        ├── layout.tsx
        └── page.tsx
    └── public
    ├── .gitignore
    ├── next.config.ts
    ├── package.json
    └── tsconfig.json

Step 1 - Create the Navbar Component

Create a new folder and file at components/Navbar/Navbar.tsx. Keeping components in their own named folders is a clean habit - it makes the project easier to navigate as it grows.

Code
└── components
    └── Navbar
        └── Navbar.tsx

Paste this starting code into Navbar.tsx:

Tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";

function Navbar() {
  const pathName = usePathname();
  console.log(pathName); // logs "/" on the home page

  const navLinks = [
    { name: "home", url: "/" },
    { name: "about us", url: "/about-us" },
    { name: "services", url: "/services" },
    { name: "blogs", url: "/blogs" },
    { name: "contact us", url: "/contact-us" },
  ];

  return (
    <nav className="flex justify-between items-center px-10 py-3 border-b border-gray-400">
      <div className="h-6">
        <Image
          src="https://res.cloudinary.com/dyswu0gin/image/upload/v1786772071/logo_lxnwhq.png"
          alt="CodeKK"
          width={1000}
          height={1000}
          className="w-full h-full object-contain"
          loading="eager"
        />
      </div>
      <ul className="flex gap-2.5">
        {navLinks.map((navLink, index) => {
          return (
            <li key={index}>
              <Link
                href={navLink.url}
                className="text-base font-medium capitalize py-1 px-2.5"
              >
                {navLink.name}
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

export default Navbar;

There are a few things worth understanding here before moving on.

"use client" at the top - usePathname is a React hook, and hooks only work in Client Components. Without "use client", Next.js treats this as a Server Component and the hook will throw an error. Any component that uses hooks, event listeners, or browser APIs needs this directive.

usePathname - this hook from next/navigation returns the current URL path as a string. On the home page it returns "/". On /about-us it returns "/about-us". It updates automatically whenever the route changes - no manual tracking needed.

The navLinks array - instead of hardcoding each link separately in JSX, we store them in an array of objects and map over them. This keeps the code clean and makes adding or removing links a one-line change.


Step 2 - Add the Navbar to Your Layout

Open app/layout.tsx and import the Navbar so it appears on every page:

Tsx
import "./globals.css";
import Navbar from "./components/Navbar/Navbar";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="h-full antialiased">
      <body className="min-h-full flex flex-col">
        <Navbar />
        {children}
      </body>
    </html>
  );
}

Placing the Navbar in the root layout means it renders on every single page without having to import it individually. The {children} slot is where each page's content goes.


Step 3 - Configure Next.js for the External Image

Since we are loading the logo from Cloudinary, you need to tell Next.js it is an allowed image domain. Open next.config.ts and add this:

Ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "res.cloudinary.com",
        pathname: "/**",
      },
    ],
  },
};

export default nextConfig;

Without this, the next/image component will refuse to load images from external domains and throw an error.


Step 4 - Create the Test Pages

To actually test the active indicator, you need pages to navigate between. Create these files:

Code
app/
├── about-us/
│   └── page.tsx
├── blogs/
│   └── page.tsx
├── services/
│   └── page.tsx
└── contact-us/
    └── page.tsx

The content for each is simple - just a heading to confirm which page you are on. Here is about-us/page.tsx:

Tsx
import React from "react";

function AboutUs() {
  return <h1 className="mt-4 text-2xl text-center">About Us</h1>;
}

export default AboutUs;

Create the same pattern for blogs/page.tsx, services/page.tsx, and contact-us/page.tsx - just change the component name and the heading text.

Now run your dev server:

Code
npm run dev

Open the browser and check the console. You should see / logged on the home page.

[IMAGE: Browser showing the CodeKK navbar with Home, About Us, Services, Blogs, Contact Us links. Console open below showing "/" logged from Navbar.tsx]

Now click "About Us" in the navbar. Watch the console - it updates to /about-us.

[IMAGE: Browser on localhost:3001/about-us showing "About Us" heading. Console showing "/" then "/about-us" logged as navigation happened]

Click "Services" - the console shows /services.

[IMAGE: Browser on localhost:3001/services showing "Services" heading. Console showing the pathname updating to "/services"]

This is usePathname working. Every time you navigate, the hook returns the new pathname and the component re-renders with the updated value. That updated value is exactly what we will use to decide which link to highlight.


Now the actual feature. The idea is simple - compare pathName with each navLink.url. If they match, apply the active style. If they do not, apply a transparent style so the layout does not shift.

Update Navbar.tsx with this one addition inside the map:

Tsx
{navLinks.map((navLink, index) => {
  const isActive = pathName === navLink.url; // this is the key line

  return (
    <li key={index}>
      <Link
        href={navLink.url}
        className={`text-base font-medium capitalize py-1 px-2.5 border-b-2 ${
          isActive ? "border-b-[#0088d4]" : "border-b-transparent"
        }`}
      >
        {navLink.name}
      </Link>
    </li>
  );
})}

Here is what each part does:

const isActive = pathName === navLink.url - a simple strict equality check. If the current URL path exactly matches this link's URL, isActive is true. Otherwise it is false. This runs for every item in the array on every render.

border-b-2 - adds a 2px bottom border to every link. This is important. If you only added the border to the active link, the other links would shift slightly in height when they become active because the border would suddenly appear and take up space. Adding border-b-2 to all links keeps the height consistent - only the color changes.

isActive ? "border-b-[#0088d4]" : "border-b-transparent" - when active, the border color is blue (#0088d4). When inactive, the border is transparent - it still exists and takes up space, but you cannot see it. This is the trick that prevents layout shift.

[IMAGE: Browser showing the CodeKK navbar with "About Us" link highlighted with a blue bottom border, all other links having no visible border]

Navigate between pages and watch the blue border follow the current route automatically.


The Complete Navbar Component

Here is the full updated Navbar.tsx:

Tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";

function Navbar() {
  const pathName = usePathname();

  const navLinks = [
    { name: "home", url: "/" },
    { name: "about us", url: "/about-us" },
    { name: "services", url: "/services" },
    { name: "blogs", url: "/blogs" },
    { name: "contact us", url: "/contact-us" },
  ];

  return (
    <nav className="flex justify-between items-center px-10 py-3 border-b border-gray-400">
      <div className="h-6">
        <Image
          src="https://res.cloudinary.com/dyswu0gin/image/upload/v1786772071/logo_lxnwhq.png"
          alt="CodeKK"
          width={1000}
          height={1000}
          className="w-full h-full object-contain"
          loading="eager"
        />
      </div>
      <ul className="flex gap-2.5">
        {navLinks.map((navLink, index) => {
          const isActive = pathName === navLink.url;

          return (
            <li key={index}>
              <Link
                href={navLink.url}
                className={`text-base font-medium capitalize py-1 px-2.5 border-b-2 ${
                  isActive ? "border-b-[#0088d4]" : "border-b-transparent"
                }`}
              >
                {navLink.name}
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

export default Navbar;

The same concept works for a sidebar, but the design is different. Instead of a bottom border on the link, a left border on the list item makes more sense visually - it is a common pattern you see in dashboards and admin panels.

Create components/Aside/Aside.tsx:

Tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

function Aside() {
  const pathName = usePathname();

  const asideUrls = [
    { name: "all categories", url: "/all-categories" },
    { name: "product cards", url: "/loader-skeletons/product-cards" },
  ];

  return (
    <aside className="shrink-0 max-w-50 w-full border-r pl-2.5 py-10">
      <ul>
        {asideUrls.map((asideUrl, index) => {
          const isActive = pathName === asideUrl.url;

          return (
            <li
              key={index}
              className={`w-full h-fit relative after:absolute after:left-0 after:top-0 after:w-0.5 after:h-full ${
                isActive ? "after:bg-primary" : "after:bg-transparent"
              }`}
            >
              <Link
                href={asideUrl.url}
                className="px-3 py-1.5 text-sm font-medium capitalize block w-full h-full"
              >
                {asideUrl.name}
              </Link>
            </li>
          );
        })}
      </ul>
    </aside>
  );
}

export default Aside;

The logic is identical - usePathname, isActive check, conditional class. But the styling approach is different.

after:absolute after:left-0 after:top-0 after:w-0.5 after:h-full - this uses Tailwind's after: pseudo-element prefix to create a thin vertical line on the left side of the list item. The after: utility applies styles to the CSS ::after pseudo-element, which is an invisible element added after the content.

isActive ? "after:bg-primary" : "after:bg-transparent" - when active, the pseudo-element gets a background color (your primary brand color). When inactive, it is transparent. Same trick as the navbar - the element always exists, only the color toggles.

Using a pseudo-element here instead of a real element or a border keeps the layout clean. The li itself does not have a border that would affect its width or spacing - the indicator is painted on top using position: absolute.


Why This Works

The entire feature is built on one idea: usePathname gives you the current route as a string. You compare that string to each link's URL. If they match, apply the active class.

No complex state management. No context. No effect. Just a hook that returns a value and a conditional class that responds to it.

The fact that usePathname is reactive - it updates on every navigation without you doing anything - is what makes this so clean. Next.js handles the tracking internally. You just read the value and use it.

Comments

Sign in to join the conversation

Related Articles

More insights you might enjoy

SSR, CSR, SSG, and ISR in Next.js - What They Are and When to Use Each One

SSR, CSR, SSG, and ISR in Next.js - What They Are and When to Use Each One

Next.js gives you four ways to render pages and each solves a different problem. This guide explains all four with real code examples and a practical decision guide.

Sadie Sink·9 min
2991
React vs Next.js: What's Actually Different and When to Use Which

React vs Next.js: What's Actually Different and When to Use Which

React and Next.js are often confused - one is a UI library, the other is a full framework built on top of it. This post breaks down the real differences: rendering strategies, routing, API routes, and when to pick one over the other.

Sadie Sink·8 min
3570
How React's useEffect Actually Works - And Why Developers Misuse It

How React's useEffect Actually Works - And Why Developers Misuse It

useEffect is one of the most used hooks in React and also one of the most misunderstood. This guide covers the dependency array, cleanup functions, the double render, and common mistakes that cause bugs.

Sadie Sink·8 min
2850
How to Build Dark and Light Mode in React Vite with Tailwind CSS and TypeScript (Step by Step)

How to Build Dark and Light Mode in React Vite with Tailwind CSS and TypeScript (Step by Step)

A complete step by step guide to building dark and light mode in React Vite using Tailwind CSS v4 and TypeScript, explaining the Context API, localStorage persistence, and why each part works the way it does.

Sadie Sink·16 min
2290
How to Fetch Data in React - A Complete Guide with Loading, Error, and Abort
Frontend

How to Fetch Data in React - A Complete Guide with Loading, Error, and Abort

Fetch API data in React the right way - loading, error, empty state, AbortController cleanup, retry and refresh. Step by step with real mistakes shown.

Sadie Sink·11 min
1970
How to Connect Supabase with Prisma ORM in Next.js
Backend

How to Connect Supabase with Prisma ORM in Next.js

Step by step guide to connecting Supabase PostgreSQL to a Next.js project using Prisma ORM - schema setup, migrations, seeding, and Prisma Studio.

Sadie Sink·10 min
1520