Skip to content

Instantly share code, notes, and snippets.

@remy
Last active April 12, 2024 08:33
Show Gist options
  • Star 78 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save remy/0dde38897d6d660f0b63867c2344fb59 to your computer and use it in GitHub Desktop.
Save remy/0dde38897d6d660f0b63867c2344fb59 to your computer and use it in GitHub Desktop.
Next.js version of `activeClassName` support.
import Link from './Link'; // our version of link

export default () => (
  <header className="Header">
    <nav>
      <Link activeClassName="active" href="/">
        <a className="some-other-class">Home</a>
      </Link>
      <Link activeClassName="active" href="/about">
        <a>About</a>
      </Link>
      <Link activeClassName="active" href="/contact">
        <a>Contact</a>
      </Link>
    </nav>
  </header>
);
@peacefulseeker
Copy link

zeti/next already have the use case with withRouter HOC.
https://github.com/zeit/next.js/tree/canary/examples/using-with-router

@emil-alexandrescu
Copy link

emil-alexandrescu commented Mar 12, 2019

Thanks a lot! Cleaned version of ActiveLink.

import { withRouter } from 'next/router';
import cx from 'classnames';
import Link from 'next/link';
import React, { Children } from 'react';

const ActiveLink = ({
  router,
  children,
  href,
  activeClassName,
  ...otherProps
}) => {
  const child = Children.only(children);
  const className = cx(child.props.className, {
    [activeClassName]: router.pathname === href && activeClassName
  });

  return (
    <Link href={href} {...otherProps}>
      {React.cloneElement(child, { className })}
    </Link>
  );
};

@hkirsman
Copy link

hkirsman commented May 1, 2019

What's that 'classnames' on second line? @emil-alexandrescu

@davideperozzi
Copy link

davideperozzi commented Jul 22, 2019

Thank you! Here is my typescript version:

import cx from 'classnames';
import { WithRouterProps } from 'next/dist/client/with-router';
import NextLink from 'next/link';
import { withRouter } from 'next/router';
import { ReactElementLike } from 'prop-types';
import { Children, cloneElement, Component } from 'react';

interface LinkProps extends WithRouterProps {
  href: string;
  children: ReactElementLike;
  activeClassName: string;
}

class Link<P extends LinkProps> extends Component<P> {
  public render() {
    const {
      router,
      children,
      href,
      activeClassName,
      ...otherProps
    } = this.props;

    const child = Children.only(children);
    const active = this.props.router.pathname === href && activeClassName;
    const className = cx(
      child.props.className,
      { [activeClassName]: active }
    );

    return (
      <NextLink href={this.props.href} {...otherProps}>
        {cloneElement(child, { className })}
      </NextLink>
    );
  }
}

export default withRouter(Link);

@ScottSmith95
Copy link

Here's a version using a Hook instead of a HOC.

import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter();

  const child = React.Children.only(children);

  let className = child.props.className || "";
  if (router.pathname === props.href && props.activeClassName) {
    className = `${className} ${props.activeClassName}`.trim();
  }

  delete props.activeClassName;

  return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
};

@goodok21
Copy link

goodok21 commented Aug 7, 2019

Here's a version using a Hook and styled-components:

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter()
  const child = React.Children.only(children)
  return (
    <Link {...props}>
      {React.cloneElement(child, { active: router.pathname === props.href })}
    </Link>
  )
}

Using example:

<ActiveLink href="/path" prefetch>
  <NavLink>Vikup auto</NavLink>
</ActiveLink>

NavLink components something like this:

const NavLink = styled.a`
  color: ${({ active }) => active ? 'red' : 'black'}; 
`

@JavierBrooktec
Copy link

Hey guys im having an issue with dynamic routing and this component.

I have a Nav component in components folder:

import Link from './Link';

export default () => (
 <nav>
   <style jsx>{`
     .active {
       color: red;
     }

     .nav-link {
       text-decoration: none;
       padding: 10px;
       display: block;
     }
   `}</style>

   <ul>
     <li>
       <Link activeClassName="active" href="/">
         <a className="nav-link home-link">Home</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/about">
         <a className="nav-link">About</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/post/[id]" as="/post/first">
         <a>First Post</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/post/[id]" as="/post/second">
         <a>Second Post</a>
       </Link>
     </li>
   </ul>
 </nav>
);

and my pages folder look like this:

pages/
|---- post/
|      ---- [id]/
|             ---- index.js     
|---- about.js
|---- index.js

and the Index.js from [id] folder:

import {useRouter} from 'next/router';
import Nav from '../../../components/Nav';

const Post = () => {
  const router = useRouter();
  const {id} = router.query;

  return (
    <React.Fragment>
      <Nav />
      <h1>Post: {id}</h1>
    </React.Fragment>
  );
};

export default Post;

the problem it's that adds right the active class in al cases except if u reload in /post/[id], if so its return this error and don't add the active class:
Warning: Prop className did not match. Server: "jsx-382284644" Client: "jsx-382284644 active"

Captura de pantalla 2019-08-28 a las 19 22 23

in my Link component im comparin the asPath insted of href like this:

import {withRouter} from 'next/router';
import Link from 'next/link';
import React, {Children} from 'react';

const ActiveLink = ({router, children, as, ...props}) => {
  const child = Children.only(children);

  let className = child.props.className || null;

  if (as) {
    if (router.asPath === as && props.activeClassName) {
      className = `${className !== null ? className : ''} ${props.activeClassName}`.trim();
    }
  } else {
    if (router.pathname === props.href && props.activeClassName) {
      className = `${className !== null ? className : ''} ${props.activeClassName}`.trim();
    }
  }

  delete props.activeClassName;

  return (
    <Link as={as} {...props}>
      {React.cloneElement(child, {className})}
    </Link>
  );
};

export default withRouter(ActiveLink);

@canrozanes
Copy link

canrozanes commented Oct 11, 2019

A version with ES6 destructuring, proptypes and useRotuer hook:

import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
import { useRouter } from 'next/router';

const ActiveLink = ({ href, activeClassName, children }) => {
  const router = useRouter();

  const child = React.Children.only(children);

  let className = child.props.className || '';
  if (router.pathname === href && activeClassName) {
    className = `${className} ${activeClassName}`.trim();
  }

  return <Link href={href}>{React.cloneElement(child, { className })}</Link>;
};

ActiveLink.propTypes = {
  href: PropTypes.string,
  activeClassName: PropTypes.string,
  children: PropTypes.node.isRequired
};
ActiveLink.defaultProps = {
  href: '',
  activeClassName: ''
};

export default ActiveLink;

@sekoyo
Copy link

sekoyo commented Jan 27, 2020

I do it like this:

const ActiveLink: NextPage<Props> = ({ children, href }) => {
  const router = useRouter();

  return (
    <Link href={href} passHref>
      <Anchor active={router.pathname === href}>{children}</Anchor>
    </Link>
  );
};

const Anchor = styled.a<{ active: boolean }>`
  color: ${props => (props.active ? 'red' : 'blue')};
`;

If full control is needed just replace Link all together, from their docs:

import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.pathname === href ? 'red' : 'black',
  }

  const handleClick = e => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink

Copy link

ghost commented Mar 14, 2020

new typescript version:

import { withRouter } from 'next/router';

const ActiveLink = ({ router, href, children }: any) => {
    (function prefetchPages() {
        if (typeof window !== 'undefined') {
            router.prefetch(router.pathname);
        }
    })();

    const handleClick = (event: React.MouseEvent<HTMLElement>) => {
        event.preventDefault();
        router.push(href);
  
    };

    const isCurrentPath = router.pathname === href || router.asPath === href;

    return (
        <a
            href={href}
            onClick={handleClick}
            className="url-item"
            style={{
                fontWeight: isCurrentPath ? 'bold' : 'normal',
                color: isCurrentPath ? '#F00' : '#fff'
            }}
        >
            {children}
        </a>
    );
};

export default withRouter(ActiveLink);

@shobhit-jain
Copy link

shobhit-jain commented Aug 16, 2020

I did something like this with typescript and tailwind css :

import { useRouter } from 'next/router'
import Link from 'next/link'

type Props = {
  href: string
  linkName: string
  activeClassName?: string
} & typeof defaultProps

const defaultProps = {
  activeClassName: 'text-green font-600',
}

export const NavLink = ({ href, linkName, activeClassName }: Props) => {
  const router = useRouter()

  return (
    <Link href={href}>
      <a className={router.pathname === href ? activeClassName : null}>
        {linkName}
      </a>
    </Link>
  )
}

NavLink.defaultProps = defaultProps

@nathansearles
Copy link

If you need support for dynamic routes ([slug], [id], [...param], [[...slug]], etc.) in Next.js. The following will check if the href contains a dynamic route and then check if the "router.asPath" is equal to the "props.as". When matched the "activeClassName" will be added to the link.

Thanks @remy!

import { useRouter } from "next/router";
import Link from "next/link";
import React, { Children } from "react";

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter();
  const child = Children.only(children);
  let className = child.props.className || "";
  const isDynamicRoute = props.href.match(/^\/?\[{1,2}\.{0,3}[a-z]+\]{1,2}$/);

  if (
    router.pathname === props.href &&
    !isDynamicRoute &&
    props.activeClassName
  ) {
    className = `${className} ${props.activeClassName}`.trim();
  } else if (router.asPath === props.as && isDynamicRoute) {
    className = `${className} ${props.activeClassName}`.trim();
  }

  delete props.activeClassName;

  return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
};

export default ActiveLink;

@TrejoCode
Copy link

A version with ES6 destructuring, proptypes and useRotuer hook:

import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
import { useRouter } from 'next/router';

const ActiveLink = ({ href, activeClassName, children }) => {
  const router = useRouter();

  const child = React.Children.only(children);

  let className = child.props.className || '';
  if (router.pathname === href && activeClassName) {
    className = `${className} ${activeClassName}`.trim();
  }

  return <Link href={href}>{React.cloneElement(child, { className })}</Link>;
};

ActiveLink.propTypes = {
  href: PropTypes.string,
  activeClassName: PropTypes.string,
  children: PropTypes.node.isRequired
};
ActiveLink.defaultProps = {
  href: '',
  activeClassName: ''
};

export default ActiveLink;

Thanks a lot, for use:

<NavLink href = "/path" activeClassName = "--active">
    <a className="classes">
        Home Page
    </a>
</NavLink>

@SSylvain1989
Copy link

SSylvain1989 commented Mar 9, 2021

Hello , work for me .
find on SO https://stackoverflow.com/questions/53262263/target-active-link-when-the-route-is-active-in-next-js :

import Link from "next/link";
import { useRouter } from "next/router";


export const MyNav = () => {

  const router = useRouter();

  return (
    <ul>
      <li className={router.pathname == "/" ? "active" : ""}>
        <Link href="/">home</Link>
      </li>
      <li className={router.pathname == "/about" ? "active" : ""}>
        <Link href="/about">about</Link>
      </li>
    </ul>
  );
};

my code :

          <Link href="/">
            <a className={router.pathname ==="/" ? styles.navlinkActive : styles.navlink}>
              Accueil
            </a>
          </Link>

@GBrachetta
Copy link

GBrachetta commented Mar 24, 2021

All these look like great solutions, but regrettably I cannot make any to work reliably with this navbar structure:

import Navbar from 'react-bootstrap/Navbar';
import Nav from 'react-bootstrap/Nav';
import ActiveLink from '../CustomLink/ActiveLink';

export default function Navigation() {
  return (
    <Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
      <ActiveLink activeClassName="active" href="/">
        <Navbar.Brand>
          <img src="/logo.svg" alt="WhiteQueen Logo" height={50} />
        </Navbar.Brand>
      </ActiveLink>
      <Navbar.Toggle aria-controls="responsive-navbar-nav" />
      <Navbar.Collapse id="responsive-navbar-nav">
        <Nav className="mr-auto">
          <ActiveLink activeClassName="active" href="/">
            <Nav.Link>Home</Nav.Link>
          </ActiveLink>
          <ActiveLink activeClassName="active" href="/publications">
            <Nav.Link>Publications</Nav.Link>
          </ActiveLink>
          <ActiveLink activeClassName="active" href="/links">
            <Nav.Link>Links</Nav.Link>
          </ActiveLink>
        </Nav>
        <Nav>
          <ActiveLink activeClassName="active" href="/contact">
            <Nav.Link>Contact</Nav.Link>
          </ActiveLink>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

If I click on a link nested in a different <Nav> (for example, "Contact") the highlighting of the previous one isn't removed...

Am I missing something obvious?

@itsjavi
Copy link

itsjavi commented Apr 21, 2021

I rather have my own function that creates a pure next/link component:

import Link from "next/link";
import {useRouter} from "next/router";
import styles from './Toolbar.module.css'

const Toolbar = () => {
    const router = useRouter();
    const activeLink = (path, content, activeClass = styles.active, normalClass = '') => {
        let className = router.pathname === path ? activeClass : normalClass;
        return <Link href={path}><a className={className}>{content}</a></Link>
    }

    return <div className={styles.toolbar}>
        <nav>
            {activeLink('/', 'Home')}
            {activeLink('/about', 'About')}
        </nav>
    </div>
}

export default Toolbar;

If you need a more reusable function, you can pass the router:

import Link from "next/link"

const createActiveLink = (router, path, content, activeClass = 'active', normalClass = '') => {
      let className = router.pathname === path ? activeClass : normalClass;
      return <Link href={path}><a className={className}>{content}</a></Link>
  }

export default createActiveLink

@su-pull
Copy link

su-pull commented Oct 30, 2021

typed. 21/10/22
Thank you very much remy san XD

import { withRouter, NextRouter  } from 'next/router';
import React, { ReactElement } from 'react';
import Link from 'next/link';

type Props = {
  router: NextRouter;
  children: ReactElement;
  href: string;
  activeClassName: string;
}

const ActiveLink = ({ router, children, ...props }: Props) => {
  const child = children;
  
  let className: string = child.props.className;
  if (router.pathname == props.href) {
    className = `${className} ${props.activeClassName}`;
  }

  return (
  <Link {...props}>{React.cloneElement(child, { className })}</Link>
  );
}

export default withRouter(ActiveLink);

@mr9d
Copy link

mr9d commented Jan 16, 2022

Here's my version of ActiveLink for Next.js + TypeScript:

import Link, { LinkProps } from "next/link";
import { NextRouter, useRouter } from "next/router";
import { FC, PropsWithChildren } from "react";

type ActiveLinkProps = LinkProps & {
  activeClassName: string;
  className: string;
};

export const ActiveLink: FC<ActiveLinkProps> = ({ children, ...props }: PropsWithChildren<ActiveLinkProps>) => {
  const router: NextRouter = useRouter();
  const className: string =
    props.className + ((router.pathname === props.href && props.activeClassName) ? " " + props.activeClassName : "");
  return (
    <Link {...props}>
      <a className={className}>{children}</a>
    </Link>
  );
};

Example of usage:

          <ActiveLink href="/" className="menulink" activeClassName="active">
            Home
          </ActiveLink>

The difference is you no longer need <a> tag inside.

Benefits:

  • TypeScript-friendly
  • You no longer need to find child element and its type with Children.only
  • You no longer need to do React.cloneElement
  • You won't forget to put <a> inside anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment