Next.js Active Nav with Only CSS
Anyone who has worked on a web application with a navigation section has probably encountered a time where they've wanted to highlight the current nav item.
I recently ran into this using Next.js and, according to their developer advocate, the recommended solution is to create a client component and add the following code:
"use client";
const pathname = usePathname();
<Link
href={pathname}
className={clsx("nav-item", {
"nav-item-active": pathname === href,
})}
>
{children}
</Link>;
It seems pretty straightforward, but it's not without a major drawback. Next is forcing us to use client-side navigation. This means that the active nav item is not active until the page is loaded and all client javascript is executed. The need for a client-side hook just to decorate an active nav item seems like the framework is asking too much of developers. For someone like me who has prioritized the adoption of server-side rendering whenever possible, the CSS part of my brain lit up. I realized that this could be accomplished without any additional client-side JavaScript or the need to create yet another component.
Enter :has()
The functional :has() CSS pseudo-class represents an element if any of the relative selectors that are passed as an argument match at least one element when anchored against this element. This pseudo-class presents a way of selecting a parent element or a previous sibling element with respect to a reference element by taking a relative selector list as an argument.
My mental model of :has() is maybe a little crude, but more concise.
- Establish a parent.
- Find a selector/condition within the parent's children.
- Go "back up" to the parent and apply the styles.
The CSS Solution
In our page components, we're going to add an id to the main elemenet.
For example, here's the RSC home page.
export default function Home() {
return (
<main id="home" className="...">
<h1>Home</h1>
</main>
);
}
And here is our RSC sidebar.
export default function Sidebar() {
return (
<aside className="...">
<h1 className="...">Side Nav</h1>
<ul className="...">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
</aside>
);
}
Finally, we'll add the CSS to highlight the active nav item.
:has(#home) aside a[href="/"] {
color: red;
}
Remember that our :has is starting from the root of the document. Once we find the #home, we "go back up" to the aside and find the a[href="/"] and apply the styles.
Pretty cool, right? Now let's recap the pros and cons of this approach.
Pros
- No client-side JavaScript!
- No need to create a new component
- No need to use
clsxortwMerge - No need to use
usePathname
Cons
- Requires a little additional markup
- Requires hardcoding the
idin themainelement to the path of the page
To this last point, you can see how the CSS needs to grow to support more pages.
:has(#home) aside a[href="/"],
:has(#about) aside a[href="/about"],
:has(#contact) aside a[href="/contact"] {
color: red;
}
If you're curious about the full implementation, you can check out the demo repo.