30.6 Active State Highlighting in Menus
Right, let’s talk about making sure your user isn’t lost. The single most important job of a navigation menu is to answer the silent, desperate question every user has: “Where the hell am I?” Active state highlighting is how you answer that question. It’s the digital equivalent of a “You Are Here” star on a mall map, and if you screw it up, your user is going to the food court out of sheer frustration, never to return to your website.
The concept is simple: the menu item for the current page should look different. It should be obvious. But as with everything in web development, the devil is in the details, and he’s wearing a surprisingly confusing hat.
The Only Correct Way: It’s All About the URL
Forget page titles or some arbitrary data- attribute you set manually. The one true source of “what page is this?” is the URL. Your highlighting logic must be a function of the current page’s URL (window.location) and the URL the menu item points to (href). Everything else is a hack waiting to break.
The most common, and frankly, most robust method is to add a class (like .active) to the list item (<li>) or the anchor (<a>) tag when their href matches the current page’s path. You can do this on the server before you send the HTML (blazingly fast, no flash of un-styled content) or with JavaScript on the client (more flexible for SPAs).
Here’s a dead-simple vanilla JS example for a multi-page site. We’ll compare the href’s pathname to the current location’s pathname.
// Get all the nav links
const navLinks = document.querySelectorAll('nav a');
// Get the current URL, standardized to its pathname
const currentUrl = new URL(window.location.href);
const currentPath = currentUrl.pathname;
navLinks.forEach(link => {
// Get the link's URL, also standardized
const linkUrl = new URL(link.href, window.location.origin);
const linkPath = linkUrl.pathname;
// Compare the paths. This is a basic comparison.
if (linkPath === currentPath) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
And the corresponding CSS to make it visually distinct:
nav a {
color: #555;
text-decoration: none;
padding: 0.5rem 1rem;
}
nav a.active {
color: #000;
background-color: #f0f0f0;
font-weight: bold;
border-left: 3px solid #ff3c00;
}
The Matching Problem: A Comedy of Errors
This is where it gets fun. Your naive linkPath === currentPath will fail spectacularly in the real world. What if your site lives in a subdirectory and your href is /about but the current page is /my-site/about? What if the URL has query parameters or a hash? You need a smarter comparison.
You need to normalize your URLs. The URL API we used above is a great start because it automatically resolves relative paths. But you might need more logic.
// A more robust function to check for an active link
function isLinkActive(linkElement) {
const linkUrl = new URL(linkElement.href, window.location.origin);
const currentUrl = new URL(window.location.href);
// Compare just the pathnames, ignoring search params and hash
if (linkUrl.pathname === currentUrl.pathname) {
return true;
}
// Optional: a fallback for the homepage.
// If we're on the root '/' and the link points to '/index.html', it's probably the same page.
if (currentUrl.pathname === '/' && linkUrl.pathname.endsWith('/index.html')) {
return true;
}
return false;
}
// Use it in our loop
navLinks.forEach(link => {
if (isLinkActive(link)) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
The SPA Quandary
If you’re building a Single Page Application (SPA) with a client-side router (React Router, Vue Router, etc.), window.location is often lying to you. These routers manipulate the browser’s history without doing a full page reload, so the URL might change but your highlighting script might only run once on page load.
The solution here is to listen for route changes. Thankfully, these frameworks give you the tools. In React Router, for example, you don’t manually add classes. You use the NavLink component, which automatically applies an active class (.active by default) when its to prop matches the current location. It’s doing all the URL matching nonsense we just wrote, but for you. Use it.
// The right way in React Router
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink to="/">Home</NavLink>
<NavLink to="/about">About</NavLink>
<NavLink to="/contact">Contact</NavLink>
</nav>
);
}
The “Almost Active” State (Breadcrumbs, I’m Looking at You)
Sometimes a parent item in a hierarchical menu should stay highlighted even when a child page is active. On an “About Us” page, the “About” top-level menu item should probably remain active. This requires a different kind of matching: checking if the current path starts with or is contained within the link’s path.
function isLinkOrChildActive(linkElement) {
const linkUrl = new URL(linkElement.href, window.location.origin);
const currentUrl = new URL(window.location.href);
// Check if the current pathname starts with the link's pathname
// This makes '/about' active for '/about', '/about/team', '/about/history', etc.
if (currentUrl.pathname.startsWith(linkUrl.pathname)) {
// But we have to be careful! What if two sections have similar names?
// '/a' would be active for '/about', which is wrong. Let's add a sanity check.
// Ensure the next character is either a slash or the end of the string.
const nextChar = currentUrl.pathname[linkUrl.pathname.length];
if (nextChar === undefined || nextChar === '/') {
return true;
}
}
return false;
}
The key takeaway? Think critically about what “active” means in each context. Don’t just copy-paste the first script you find on Stack Overflow. Test it. Break it. Make sure your user never, ever has to ask that dreaded question.