Wordpress adds .current-menu-item class to menu items linked to page sections on home page

I have the following menu items

home https://test.test/

about https://test.test/#about

services https://test.test/#services

#about and #services are sections on home page. Because #about and #services URLs are pointing to the home page, wordpress adds .current-menu-item class to all 3 above URLs at the same time.

I was wondering how can I have that class added only when home or about or services is clicked? I use .current-menu-item class to style currently active menu item.

I tried the following solution but it didn’t work

 function  add_menu_item_classes( $classes, $item, $args ) {

	if( 'header' !== $args->theme_location )
 		return $classes;

 	if ( is_singular() && 'about' == $item->title ) 
        $classes[] = 'test';
    

	if ( is_singular() && 'services' == $item->title ) 
        $classes[] = 'test';
		
	return array_unique( $classes );
}
add_filter( 'nav_menu_css_class', 'add_menu_item_classes', 10, 4 );

Hi,

The problem is that since all your menu links point to the same URL (/ with different #fragment identifiers), WordPress sees them all as the same page and adds .current-menu-item to all of them. PHP can’t really help here — it never sees the #about or #services parts, so this is a front-end problem.

Here’s a small JavaScript snippet that will highlight only the item you click:

document.addEventListener("DOMContentLoaded", () => {
  const menuItems = document.querySelectorAll(".menu-item a");

  menuItems.forEach(link => {
    link.addEventListener("click", () => {
      document.querySelectorAll(".menu-item").forEach(item =>
        item.classList.remove("current-menu-item")
      );
      link.parentElement.classList.add("current-menu-item");
    });
  });
});

Try this on your site and let me know if it comes close to what you want. It won’t persist after reload or navigation, but it may be enough depending on what you’re after.

Also, is the page live anywhere? If so, it’s be great if you could post a link (or DM it to me).

1 Like

Depending on how the page is handling using the fragments, it may be simpler to use :target?

1 Like

http://test.prygara.com/
I’ve added the JS script that you suggested. It only appears to be working when you are on a home page and click ‘about’ or ‘services’. In all other scenarios - all 3 menu items on home page are still highlighted (underlined). You can try click through menu items back and forth and see how it behaves.

:target can style the section itself, but it can’t affect the menu item that links to it. So unfortunately it won’t help here for controlling .current-menu-item in the navigation.

I’ve adjusted the JavaScript to handle the fragment (#about, #services) on page load. It reads the URL and sets the correct menu item active based on that, removing the highlight from the others.

document.addEventListener("DOMContentLoaded", () => {
  if (window.location.pathname === "/") {
    const menuItems = document.querySelectorAll(".menu-item");
    const currentHash = window.location.hash;

    // Clean up all current-menu-item classes first
    menuItems.forEach(item => item.classList.remove("current-menu-item"));

    if (currentHash) {
      // Look for a menu link that matches the hash
      const match = document.querySelector(`.menu-item a[href$="${currentHash}"]`);
      if (match) {
        match.parentElement.classList.add("current-menu-item");
      }
    } else {
      // No hash – assume it's the Home link (href="/")
      const homeLink = document.querySelector('.menu-item a[href="/"]');
      if (homeLink) {
        homeLink.parentElement.classList.add("current-menu-item");
      }
    }

    // Handle clicks
    document.querySelectorAll(".menu-item a").forEach(link => {
      link.addEventListener("click", () => {
        menuItems.forEach(item => item.classList.remove("current-menu-item"));
        link.parentElement.classList.add("current-menu-item");
      });
    });
  }
});

Let us know if that works for you.

squints
If you added the menu-item-# class to the sections, it could work for services and about? (Would still have to work something for the null case… eh.)

.page:has(.menu-item-60:target) header .menu-item-60,
.page:has(.menu-item-134:target) header .menu-item-134,
.page:has(.menu-item-135:target) header .menu-item-135,
.menu-item-56.current-menu-item,
.menu-item-59.current-menu-item {
//...
}

?

(Yeah, its a bit fragile, but it would work without javascript…)

Side note: Should probably use the empty fragment instead of no-fragment for the Home link, if you’re going to use fragment navigation like that. Saves a reload of the page when clicking home from services/about.

EDIT: I suppose the null case could be .page:not(:has(:target)) .menu-item-60 ? (No Marc, that doesnt work for the other pages… try again)
.page:not(:has(:target)) .current_page_item.menu-item-60

Ah, I see what you’re going for now. If you add .menu-item-### classes to the corresponding sections, your :has(:target) selectors can map the active section back to the nav item without JavaScript. :nerd_face:

Your revised fallback using .page:not(:has(:target)) .current_page_item.menu-item-60 is a better call — relying on WordPress to flag the actual page does help avoid highlighting Home on unrelated pages like /gallery.

The main drawback for me is that it’s tightly coupled to specific menu item IDs and assumes a strict structure — meaning your CSS selectors depend on exact .menu-item-### classes, and you’d need to manually mirror those classes on the corresponding sections.

yeah, thats what i meant by it being “fragile”… if the menu items change (I assume Wordpress generates the IDs once, but maybe it ‘refreshes’ at certain triggers? I dont know.) then the house of cards comes tumbling down unless you can specify a class to always be put on each individual element. Wordpress tries to do this with the “.menu-item-(pagename)” class, but it doesnt do it uniquely for the other anchors on the page, sadly.

1 Like

Makes sense — and yes, the menu item IDs are persistent unless you delete/recreate them, but there’s always that lingering possibility that someone edits the menu later without realizing what the CSS depends on.

It’s a shame WordPress doesn’t auto-generate semantic classes like .menu-item-about for custom anchor links. If that were possible, the whole approach would be much cleaner and easier to maintain.

Although, that said, something like this might work (untested):

add_filter( 'nav_menu_css_class', function( $classes, $item ) {
  if ( $item->type === 'custom' && $item->url ) {
    $parsed = wp_parse_url( $item->url );
    if ( isset( $parsed['fragment'] ) ) {
      $classes[] = 'menu-item-' . sanitize_html_class( $parsed['fragment'] );
    }
  }
  return $classes;
}, 10, 2 );

This would give you:

<nav id="site-navigation" class="main-navigation">
  <ul id="header" data-visible="false" class="menu">
    <li id="menu-item-60" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-home current-menu-item page_item page-item-20 current_page_item menu-item-60">
      <a href="https://test.prygara.com/" aria-current="page">home</a>
    </li>
    <li id="menu-item-134" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-134 menu-item-about">
      <a href="http://test.prygara.com/#about" aria-current="page">about</a>
    </li>
    <li id="menu-item-135" class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-135 menu-item-services">
      <a href="http://test.prygara.com/#services" aria-current="page">services</a>
    </li>
    <li id="menu-item-56" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-56">
      <a href="https://test.prygara.com/gallery/">gallery</a>
    </li>
    <li id="menu-item-59" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-59">
      <a href="https://test.prygara.com/contact/">contact</a>
    </li>
  </ul>
</nav>

Note, these classes were added:

  • menu-item-about to the About item
  • menu-item-services to the Services item

You can now target those in CSS like:

.page:has(#about:target) .menu-item-about { ... }
.page:has(#services:target) .menu-item-services { ... }

Like I said, all untested, but this might be the beginnings of a better solution?

1 Like

It appears the following portion of the script is not working. When ‘home’ is clicked current-menu-item class is not added to that menu item. Rest of menu items work as expected.

else {
      // No hash – assume it's the Home link (href="/")
      const homeLink = document.querySelector('.menu-item a[href="/"]');
      if (homeLink) {
        homeLink.parentElement.classList.add("current-menu-item");
      }

I’ve updated the test site link. Please take a look once you get a chance.

This filter also works but I am not sure how to target ‘home’ menu item when it is clicked so it gets highlighted. Basically each menu item should get highlighted separately on click.

add_filter( 'nav_menu_css_class', function( $classes, $item ) {
  if ( $item->type === 'custom' && $item->url ) {
    $parsed = wp_parse_url( $item->url );
    if ( isset( $parsed['fragment'] ) ) {
      $classes[] = 'menu-item-' . sanitize_html_class( $parsed['fragment'] );
    }
  }
  return $classes;
}, 10, 2 );

It seems that when you’re already on the homepage (including if you’re at #about or #services) and you click “Home,” the browser reloads the page. That wipes out the highlight that JavaScript had just set.

And when you’re coming from another page, like Gallery or Contact, then the reload is legit. But JavaScript kicks in and clears the highlighting and then nothing gets re-highlighted, because there’s no fragment.

Try updating the JavaScript like this:

document.addEventListener("DOMContentLoaded", () => {
  const menuItems = document.querySelectorAll(".menu-item");
  const menuLinks = document.querySelectorAll(".menu-item a");
  const currentHash = window.location.hash;
  const currentPath = window.location.pathname;

  // Clean up all current-menu-item classes first
  menuItems.forEach(item => item.classList.remove("current-menu-item"));

  // Highlight based on hash or fallback to Home
  if (currentHash) {
    const match = document.querySelector(`.menu-item a[href$="${currentHash}"]`);
    if (match) match.parentElement.classList.add("current-menu-item");
  } else if (currentPath === "/") {
    const homeLink = Array.from(menuLinks).find(link =>
      link.href === window.location.origin + "/" || link.href === window.location.href
    );
    if (homeLink) homeLink.parentElement.classList.add("current-menu-item");
  }

  // Intercept clicks
  menuLinks.forEach(link => {
    link.addEventListener("click", event => {
      // Prevent reload if already on homepage and clicking Home
      if (
        link.getAttribute("href") === "/" &&
        window.location.pathname === "/" &&
        !window.location.hash
      ) {
        event.preventDefault();
      }

      // Set active state
      menuItems.forEach(item => item.classList.remove("current-menu-item"));
      link.parentElement.classList.add("current-menu-item");
    });
  });
});

Ah, it’s probably the case that the filter works for adding custom classes on fragment links, but it won’t apply anything to the Home menu item, since that’s not a custom link with a fragment.

If you want to give the Home item a similar semantic class, you can extend the filter like this:

add_filter( 'nav_menu_css_class', function( $classes, $item ) {
  if ( $item->type === 'custom' && $item->url ) {
    $parsed = wp_parse_url( $item->url );
    if ( isset( $parsed['fragment'] ) ) {
      $classes[] = 'menu-item-' . sanitize_html_class( $parsed['fragment'] );
    }
  } elseif ( $item->object_id && get_permalink( $item->object_id ) === home_url( '/' ) ) {
    $classes[] = 'menu-item-home-link';
  }
  return $classes;
}, 10, 2 );

Now you should get .menu-item-home-link on the Home item as well (still untested).

1 Like

This version only works on first 3 menu items. It does quickly highlight Gallery or Contact but then highlight disappears.

This version keeps home item highlighted persistently so when next item is clicked I have 2 items highlighted. Also I didn’t figure out how to highlight gallery and contact menu items with this version of filter.

Selectors I used

.menu-item-home-link a:after,
.page:has(#about:target) .menu-item-about a:after, 
.page:has(#services:target) .menu-item-services a:after {

Sorry, I know that must be quite frustrating, especially when it’s close to working but still behaving weirdly. At this point, I’m shooting in the dark a bit because there might be something else in play (theme CSS, JS, or how WordPress is outputting the menu).

Give me a couple of days and I’ll spin up a WordPress install on my end and try to replicate exactly what you’re seeing. That way I can test everything properly and stop guessing.

1 Like

cough :wink:

.page:not(:has(#about:target)):not(:has(#services:target)) .current_page_item.menu-item-home:nth-of-type(1) a:after
?
(Still quasi-fragile, and a helluva mouthful, but assuming you always keep “Home” first in the nav…)

EDIT: Also Marc, think shorter. You’re targetting the lack of a target entirely.
.page:not(:has(:target)) .current_page_item.menu-item-home:nth-of-type(1) a:after

For the benefit of explanation:

.page:not(:has(:target)) .current_page_item.menu-item-home:nth-of-type(1)
 a:after

If there is no targeted element (the hashtag is missing, empty, or doesn’t have a matching element),
And the current page is the Home page (because .current_page_item will only be set on Home, Services, and About at that point, this would select all 6 of them [because you’ve got a nav at the top and bottom, so 6 elements inside 2 different parents])
But only the first element you selected inside each parent.
Inside that element, find all links, and target their :after pseudoelements.

1 Like

That’s all good. Take your time. Thanks @James_Hibbard

Thanks @m_hutley - now menu items get selected correctly with modified (extended) filter by @James_Hibbard.

Selectors I used with that filter

.page:not(:has(:target)) .current_page_item.menu-item-home:nth-of-type(1)
 a:after,
.page:has(#about:target) .menu-item-about a:after, 
.page:has(#services:target) .menu-item-services a:after,
.page:not(:has(:target)) .current_page_item:nth-of-type(4)
 a:after,
.page:not(:has(:target)) .current_page_item:nth-of-type(5)
 a:after {...}

Oh good. Are you happy with that solution? Or do you still want me to take a look at the JS side of things?