DEV Community

Mayo
Mayo

Posted on

How to Target and Style the Active Link in Next.Js (With Typescript)

Most React projects use React Router's activeClassName to target active routes. But building a navigation component in Next.JS with a styled active link isn't as straightforward.

In Next.js, the built-in <Link> component requires customization to achieve a similar effect.

Let's explore two solutions using Typescript: a basic one and a detailed (recommended) one.

Basic Solution

This is an example of a basic solution which uses a custom ActiveLink component and the useRouter hook.

//Set up your navigation component with a custom 'ActiveLink' component (imported) from a separate file.
// Then create a page route file and component for each 'href' path i.e. index.tsx, about.tsx, products.tsx 

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink href="/">
            Home
          </ActiveLink>
        </li>
        <li>
          <ActiveLink href="/about">
            About
          </ActiveLink>
        </li>
        <li>
          <ActiveLink
            href="/products/"
          >
            Products
          </ActiveLink>
        </li>     
      </ul>
    </nav>
  );
};

export default Nav;


Enter fullscreen mode Exit fullscreen mode

Next, let's build the ActiveLink component to recreate active link behavior.

import { useRouter } from 'next/router'
import { LinkProps } from 'next/link';

//LinkProps is a type that requires 'href' as a prop. We're extending it to include a react element as a children prop.
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
}

// href is the url path passed as a prop in the Nav component. The children are the string names passed in-between the ActiveLink tags.
function ActiveLink({ children, href }: ActiveLinkProps) {

// Deconstruct `asPath` from the router object to access the current page path shown in your browser (including the search params).
  const {asPath} = useRouter()

  //define the styling for the active link. If the current page path matches the 'href' path provided, display a red link. All other links will be black.
  const style = {
    color: asPath === href ? 'red' : 'black',
  }

  // Navigate to the page provided as 'href' when the link is clicked (router.push is used for client-side transitions)
  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  //the active link will have a style of 'color:red' 
  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink
Enter fullscreen mode Exit fullscreen mode

This is a decent solution. But what if we want to scale our app to include server-side rendering, dynamic routes, custom Link props, and much more?

Here are some more tweaks to our ActiveLink component:

Recommended Solution

First, in your Nav component add a activeClassName prop with an active string to the ActiveLink component of each page route.

You can also add a dynamic "catch-all" route for nesting pages within /products i.e. /products/categories. Make sure to create corresponding page routes in the pages folder like so:

  • pages
    • products
      • [...slug] // default page for all "catch-all" routes
      • index.tsx //default home page for /products

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink activeClassName="active" href="/">
            <a>Home</a>
          </ActiveLink>
        </li>
        .....
        //add the 'activeClassName' to each ActiveLink as shown in the previous section.
       ...... 

       // this is an example of a dynamic route using query paramaters.
        <li>
          <ActiveLink
            activeClassName="active"
            href="/products/[...slug]"
            as="/products/categories?limit=5"
          >
            <a>Products Categories </a>
          </ActiveLink>
        </li>
      </ul>
    </nav>
  );
};

export default Nav;

Enter fullscreen mode Exit fullscreen mode

Second, let's revamp our ActiveLink component to take into account the activeClassName prop and additional props you may pass in the future.

We also need to ensure that asPath from the useRouter hook doesn't lead to a mismatch of routes if the page is rendered using server-side rendering.

To avoid this, Next.js docs recommends the use of isReady: a boolean used to check whether the router fields are updated on the client-side.

import { useRouter } from 'next/router';
import Link, { LinkProps } from 'next/link';
import React, { useState, useEffect, ReactElement, Children } from 'react';

//Add the activeClassName as a required prop
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
  activeClassName: string;
};

const ActiveLink = ({
  children,
  activeClassName,
  ...props
}: ActiveLinkProps) => {

  //deconstruct 'isReady' from the useRouter hook.
  const { asPath, isReady } = useRouter();

  //create an empty string as the default className of the component
  const [className, setClassName] = useState('');

  useEffect(() => {
    // isReady checks if the router fields are updated client-side (it must be used inside a useEffect hook)
    if (isReady) {

      // URL().pathname will help to eliminate query and hash strings from the url. 
      // Props.as targets dynamic routes, whilst props.href targets normal static page routes.

      const linkPathname = new URL(
        (props.as || props.href) as string,
        location.href
      ).pathname;

      // Here we make use of 'asPath' in the correct context (once 'isReady' is true)
      const activePathname = new URL(asPath, location.href).pathname;


      // Attach the activeClassName to the matching current page 
      const newClassName =
        linkPathname === activePathname
          ? `${activeClassName}`: '';

      // Sets a new 'className' state if there is a mismatch between the current and previous state. This ensures a 'toggle' like behavior between link changes.
      if (newClassName !== className) {
        setClassName(newClassName);
      }
    }
// useEffect dependencies defined below
  }, [
    asPath,
    isReady,
    props.as,
    props.href,
    activeClassName,
    setClassName,
    className,
  ]);

  return (
     // return the in-built Next Link including a child (a clone the 'a' element (child) including the activeClassName if it is the active page)
    <Link {...props}>
      {React.cloneElement(child, {
        className: className || null,
      })}
    </Link>
  );
};

export default ActiveLink;



Enter fullscreen mode Exit fullscreen mode

Finally, add styling to .active in the global css stylesheet (typically imported into _app tsx).


.active {
  color: red;
}

.active:after {
  content: ' (current page)';
}

Enter fullscreen mode Exit fullscreen mode

You should see something like this...
Next Js Active Link

Recap

A simple solution to target and style an active link in Next.Js is to create a custom Link component which utlizes the useRouter hook to access the current path and returns a Link component with an activeClassName.

This activeClassName can be styled via css to display the active link of the page route.

Top comments (0)