0

I'm trying to add a .current-menu-item class to a li when on a specific url "/portfolio/". I can achieve this with jQuery:

jQuery(document).ready(function( $ ) {
  var loc = window.location.href; // returns the full URL
  if(/portfolio/.test(loc)) {
    $('#menu-item-33').addClass('current-menu-item');
  }
});

However would like to do with PHP. This what I have so far:

function add_class_to_specific_menu_items( $atts, $item, $args ) {
    // check if the item is set to target="_blank"
    if ( $item->url == '/portfolio/' ) {
      // add the desired attributes:
      $atts['class'] = 'current-menu-item';
    }
    return $atts;
}
add_filter( 'nav_menu_link_attributes', 'add_class_to_specific_menu_items', 10, 3 );

Not sure if this is viable and/or if I can have it add the class to a specific li "#menu-item-33

8
  • Hi! Why you dont want to add a class to menu item on menu edit page in wp admin? – anton Jun 2 at 20:44
  • 2
    Doesn't WP do this automatically if you've set the nav menu and the page correctly? – Tom J Nowell Jun 2 at 21:20
  • @TomJNowell Yes you are correct, however /portfolio/ items aren't part of a archive page, so there is no current-menu-ancestor or current-page-ancestor classes on the li. – moodyjive Jun 3 at 11:32
  • couldn't you make them be a part of an archive? It sounds like /portfolio is a page template and you're using a secondary post loop to display things, rather than using a CPT with a rewrite rule of /portfolio/xxxxxxx and archive-portfolio.php – Tom J Nowell Jun 3 at 13:23
  • @TomJNowell if I could do that I would. /portfolio is not a page template. I inherited this site and am just trying to come up with a solution. – moodyjive Jun 3 at 14:51
1
+50

Here is the solution.

To set it up:

Paste the following at the bottom of the file functions.php in the folder of your current theme.

Edit by the 🟢 in the code`

$when_on_one_of_these_page_slugs should be an array of strings. The strings are the slugs of the pages on where you want the rule to kick in. The script will kick in the rule if you are on:

  • One of these slugs.
  • A child of any of these slugs.

Edit by the 🟣 in the code:

$change_these_nav_menu_items should be a list of menu item id's, of the item you want to look selected, when the rule kicks in.

You can find the ID of the menu in dev tools by right clicking on the menu item and choose "inspect element". You will see the source code of the generated page, and you will find the item ID in the <li> tag wrapping the <a> of the item you inspected.

enter image description here

/**
 * Makes specific menu item look active when visiting a certain page
 *
 * @param array $wp_nav_meny Array of items from a WP Nav Menu.
 *
 * @return array modified array of WP Nave Menu items.
 * @see https://wordpress.stackexchange.com/questions/389050/how-can-i-add-a-class-to-a-nav-li-depending-on-url
 */
function make_x_active_when_on_y( array $wp_nav_meny ) {


    // 🟢 Set an array of page slug (strings) to trigger the rule.
    $when_on_one_of_these_page_slugs = array( 'portfolio', 'add-any-slug-that-should-trigger-the-rule');
    // 🟣 Set an array menu item id's (ints) to apply the rule to.
    $change_these_nav_menu_items = array( 33, 999);

    // Get the ID's of the pages we added in the setting.
    $post_ids_to_trigger_rule = wp_list_pluck(array_map('get_page_by_path',  $when_on_one_of_these_page_slugs ), 'ID');
    // Get all the ancestors of the page we are curently visiting.
    $ancestors_of_current_page = get_post_ancestors(get_queried_object_id());
    // Add the current page into the array of ancestors.
    array_push($ancestors_of_current_page, get_the_ID());

    $new_menu = array();

    // Loop through the nav menu items.
    foreach ( $wp_nav_meny as $menu_item ) {
        /*
         * If the ID of the current page, or any of it's ancestors,
         * exists in the array of ID's that should trigger the rule,
         * AND
         * The current item ID in the loop, is in the array if nav menu
         * item id's that sould get the "current-menu-item" class.
         */
        if ( array_intersect($post_ids_to_trigger_rule, $ancestors_of_current_page) && in_array( (int) $menu_item->ID, $change_these_nav_menu_items, true ) ) {
            $menu_item->classes[] = 'current-menu-item';
        }
        $new_menu[] = $menu_item;
    }
    return $new_menu;
}
add_filter( 'wp_nav_menu_objects', 'make_x_active_when_on_y' );

How it works

When you visit a page that has the slug you set at the 🟢, or a page that has that page as an ancestor, then the rule kick in.

The rule is that the menu item that you set at 🟣 will have the class current-menu-item.

Below you can see a recording of the result.

This is a completely fresh installation of WordPress 5.7.2, running PHP 7.4.1. No plugins installed. The theme is twentytwentyone (the ones that is shipped with WordPress).

My config looked like this:

$when_on_one_of_these_page_slugs = array( 'portfolio'); // The slug of my portfolio page.
$change_these_nav_menu_items = array( 24 ); // The item ID of my "A link to Google"-menu item.

In action

10
  • A silly technicality here, but food for thought. It looks like the JS provided by the author cares more about if the slug is present in the URL, not if the menu item has the slug. This matters because (in example) the post has the slug "portfolio" but the URL is "/my/fancy/url". In that case, the author is representing that they do not want the list item to have the class specified. Or if the menu item is of the slug "yo-madoff" but the URI is "/what/happened/to/my/401k/portfolio/i/m/broke" your code wouldn't add the class though the author's JS represents it should. – Mort 1305 Jun 12 at 21:17
  • My code does exactly what the author asked for. How exactly do you mean that you would have the slug "portfolio", but not have portfolio in the url? The slug of a post is the unique identifier, and the last part of the URL. In the case of "/my/fancy/url/", the slug would be "url". If you try to change the slug of a post in WordPress to "my/fancy/url", it would save it as "my-fancy-url". – Fabian Mossberg Jun 12 at 21:33
  • Thanks @FabianMossberg. Unfortunately it doesn't seem to work. – moodyjive Jun 14 at 0:56
  • 2
    @FabianMossberg II just want to thank you for the way you presented this. It is very clear and detailed and while I don't totally understand PHP, it makes perfect sense to me. Unfortunately it still doesn't work for me, but none of the solutions are working (BTW - I don't have multiple solutions in - I try each individually). There must be some conflict with a plugin or the theme. – moodyjive Jun 15 at 10:50
  • 1
    Thanks @bosco 🙏, I use phpcs and the WordPress Coding Standards with automatic formatting in VS Code, it helps me to keep my code consistent and beautiful automatically. ✌️ – Fabian Mossberg Jun 16 at 16:35
0

Try the following tested solutions they are working 100% on the frontend screen because both solutions are based on the wp filter hook wp_get_nav_menu_items which is executed where the wp_get_nav_menu_items() function is called.

Solution 01: Add CSS class by comparing menu item ID

function add_class_to_specific_menu_items( $items, $menu, $args ){
    foreach ( $items as $index => $menu_item ){
        // conditional statement to compare the menu-item id
        if( $menu_item->ID == 33 ){
              // conditional statement to check the current url is same
              if( get_permalink() == $menu_item->url ){
                  $menu_item->classes[] = 'current-menu-item';
              }     
        }
    }
  return $items;
}
add_filter('wp_get_nav_menu_items', 'add_class_to_specific_menu_items', 10, 3);

Solution 02: Add CSS class by comparing menu item URL

function add_class_to_specific_menu_items( $items, $menu, $args ){
    foreach ( $items as $index => $menu_item ){
        // conditional statement to compare the menu-item URL
        if( $menu_item->url ==  get_site_url().'/portfolio/' ){
               $menu_item->classes[] = 'current-menu-item';
        }
    }
  return $items;
}
add_filter('wp_get_nav_menu_items', 'add_class_to_specific_menu_items', 10, 3);
4
  • Sorry it doesn't seem to work for me. While PHP isn't my speciality, how is this adding the class .current-menu-item to the specific menu item I am trying to target (#menu-item-33)? – moodyjive Jun 12 at 18:20
  • The if statement is a place where you can compare the menu-item properties, like URL or id, or title, etc. make it simple, enable the WordPress debugging log, and then add the following code before the if statement. error_log( print_r($menu_item, true)); and then check the $menu_item array in the debug log file inside wp-content -> debug.log file. Thanks – Ejaz UL Haq Jun 13 at 1:44
  • Hi - I tried both solutions. Solution 2 I believe is the same as your first answer and unfortunately doesn't do anything (I tried again). Solution 01 adds the required class to #menu-item-33, however it is always added, but I only want it added when the site url has '/portfolio/' in it. Is there anyway to add another if statement to Solution 01 that only fires when the url is '/portfolio/' ? – moodyjive Jun 14 at 0:51
  • Sure @moodyjive, Solution 01 is updated with the current page URL comparison conditional statement. Kindly check. – Ejaz UL Haq Jun 14 at 8:42
0

@modyjive -- the answer @ejaz-ul-haq gave is congruent with your PHP code given. Your PHP code tries to add the current-menu-item class to anchors that contain the /portfolio/ URL in their HREF attribute. Your code isn't quite right, so @ejaz-ul-haq fixed it up so that it gives the represented effect. But, I'm interpreting your question to mean something different: you want to convert the functionality of the JavaScript provided into something WordPress-able. If that's what you want, here's my answer.

We first determine what your JS does:

jQuery(document).ready(function( $ ) {                    // When the page loads,
    var loc = window.location.href;                       // save the current URL
    if(/portfolio/.test(loc)) {                           // and check if it contains '/portfolio/'.
        $('#menu-item-33').addClass('current-menu-item'); // If so, add `current-menu-item` to the class list of all elements with id="menu-item-33"
    }
});

Immediately, we run into a problem: your JS will add current-menu-item to any and all HTML elements having an id="menu-item-33" attribute. So, it will happen in the backend as well as the frontend and in some places you might not even know exist. I'm going to go out on a limb here, but I'm guessing you're using the nav_menu_link_attributes hook because you're trying to manipulate navigation menus printed on the front-end, maybe? If so, let's dig into the WordPress documentation to see how those are printed out. (I've provided links in the text below so you can follow along; if you're not familiar with tracing WordPress code, now's your chance to learn.)

First, we figure out a starting point. You used the nav_menu_link_attributes hook, so let's start there. From the reference guide, it is learned this hook is found in the /wp-includes/class-walker-nav-menu.php file. According to the header notes of the hook, nav_menu_link_attributes "Filters the HTML attributes applied to a menu item’s anchor element." The anchor element (<a>) is located inside the list item element (<li>). Your JavaScript represents that you want to add a class to the list item element, not the anchor. If we scroll up to line 170, we can see where the list item is added to the output stream. That line looks like this:

$output .= $indent . '<li' . $id . $class_names . '>';

We can see on lines 167 and 168 where $id is generated and on lines 153 and 154 where $class_names is generated. By hooking into the nav_menu_css_class filter (on line 153), we can miniplate the $class_names variable before it is output in the <li> opening tag on line 170, which I believe is what you want! According to the WordPress reference, the nav_menu_css_class hook "Filters the CSS classes applied to a menu item’s list item element." Yup, that's what your JS does, so we're on the right track.

So, here we go:

function add_class_to_specific_menu_items($classes, $item, $args, $depth){
    global $wp;
    $loc = add_query_arg($wp->query_vars, home_url($wp->request)); // Save the current URL (like you did in your JS)
    if(stripos($loc, '/portfolio/') !== NULL                       // and check if it contains `/portfolio/`  (like you did in your JS).
    && $item->ID === 33                                            // Also check that this is menu item #33  (like what you did in your JS, but limit only to navigation menus)
    ) {
        $classes[] = 'current-menu-item';                          // and if so, add the desired class to the list item (like you did in your JS).
    }
    // No matter what, return the $classes array or we'll break things when navigation menus are output.
    return $classes;
}
add_filter('nav_menu_css_class', 'add_class_to_specific_menu_items', 10, 4);

Time for a beer (and hopefully you'll click the answered checkmark, upvote, and give me your bounty).

5
  • Thank for your answer, however I am receiving errors: Your PHP code changes were rolled back due to an error on line 25 of file wp-content/themes/uncode-child/functions.php. Please fix and try saving again. Uncaught ArgumentCountError: Too few arguments to function add_class_to_specific_menu_items(), 3 passed in wp-includes/class-wp-hook.php on line 292 and exactly 4 expected in wp-content/themes/uncode-child/functions.php:25 Stack trace: – moodyjive Jun 14 at 0:40
  • #0 wp-includes/class-wp-hook.php(292): add_class_to_specific_menu_items(Array, Object(WP_Post), Object(stdClass)) #1 wp-includes/plugin.php(212): WP_Hook->apply_filters(Array, Array) #2 wp-content/themes/uncode/core/inc/wp-bootstrap-navwalker.php(140): apply_filters('nav_menu_css_cl...', Array, Object(WP_Post), Object(stdClass)) #3 wp-includes/class-wp-walker.php(144): wp_bootstrap_navwalker->start_el('', Object(WP_Post), 0, Object(stdClass)) #4 wp-content/themes/uncode/core/inc/wp-bootstrap-navwalker.php(238): Walker->display_element(Object(WP_Post), Array, 0, 0, Array, '') #5 /nas/co – moodyjive Jun 14 at 0:40
  • @moodyjive -- You failed to update the filtering line in your functions.php file: you must use the number "4" as demonstrated in my code, not the number "3" as you originally used. – Mort 1305 Jun 14 at 15:53
  • Thanks, but still got an error: Uncaught ArgumentCountError: Too few arguments to function add_class_to_specific_menu_items(), 3 passed in wp-includes/class-wp-hook.php on line 292 and exactly 4 expected in wp-content/themes/uncode-child/functions.php:24 Stack trace: – moodyjive Jun 14 at 16:24
  • @moodyjive -- The error your are receiving has to do with the code that is in your functions.php file on line 24. The hook is sending 4 arguments and the function you defined only has three: this error will continue so long you have this conflict in your code. – Mort 1305 Jun 24 at 18:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.