Skip to content

Instantly share code, notes, and snippets.

@davidwebca
Last active October 16, 2024 12:25
Show Gist options
  • Save davidwebca/a7b278bbb0c0ce1d1ec5620126e863bb to your computer and use it in GitHub Desktop.
Save davidwebca/a7b278bbb0c0ce1d1ec5620126e863bb to your computer and use it in GitHub Desktop.
Allow adding custom classes to WordPress menu ul, li, a and at different depths. Perfect for TailwindCSS and AlpineJS usage.
<?php
/**
* WordPress filters to allow custom arguments to wp_nav_menu to,
* in turn, allow custom classes to every element of a menu.
*
* You can apply a class only to certain depth of your menu as well.
*
* The filters use the depth argument given by WordPress
* which is an index, thus starts with level 0 (zero).
*
* Custom Arguments supported :
* link_atts or link_atts_$depth -> Add any attribute to <a> elements
* a_class or a_class_$depth -> Add classes to <a> elements
* li_class or li_class_$depth -> Add classes to <li> elements
* submenu_class or submenu_class_$depth -> Add classes to submenu <ul> elements
*
* Ex.: add a "text-black" class to all links and "text-blue" class only to 3rd level links
* wp_nav_menu([
* 'theme_location' => 'primary_navigation',
* 'a_class' => 'text-black',
* 'a_class_2'
* ...
* ])
*
* Ex.: More complete example with some TailwindCSS classes and AlpineJS sugar
* wp_nav_menu([
* 'theme_location' => 'primary_navigation',
* 'menu_class' => 'relative w-full z-10 pl-0 list-none flex',
* 'link_atts_0' => [
* ":class" => "{ 'active': tab === 'foo' }",
* "@click" => "tab = 'foo'"
* ],
* 'li_class' => 'w-full',
* 'li_class_0' => 'mb-12',
* 'a_class' => 'text-sm xl:text-xl text-white border-b hover:border-white',
* 'a_class_0' => 'text-3xl xl:text-5xl relative dash-left js-stagger a-mask after:bg-primary',
* 'li_class_1' => 'js-stagger a-mask after:bg-primary hidden lg:block',
* 'a_class_1' => 'flex h-full items-center uppercase py-2 relative border-white border-opacity-40 hover:border-opacity-100',
* 'submenu_class' => 'list-none pl-0 grid grid-cols-1 lg:grid-cols-2 lg:gap-x-12 xl:gap-x-24 xxl:gap-x-32',
* 'container'=>false
* ])
*
* @author davidwebca
* @link https://gist.github.com/davidwebca/a7b278bbb0c0ce1d1ec5620126e863bb
*/
/**
* Add custom attributes or classes to links in wp_nav_menu
*/
add_filter( 'nav_menu_link_attributes', function ( $atts, $item, $args, $depth ) {
if (property_exists($args, 'link_atts')) {
$atts = array_merge($atts, $args->link_atts);
}
if (property_exists($args, "link_atts_$depth")) {
$atts = array_merge($atts, $args->{"link_atts_$depth"});
}
if(empty($atts['class'])) {
$atts['class'] = '';
}
$classes = explode(' ', $atts['class']);
/**
* Fix for tailwindcss classes that include ":" (colon)
* Enter triple underscore hover___text-primary instaed of hover:text-primary
*
* Some filters provided so that you can customize your own replacements,
* passed directly to preg_replace so supports array replacements as well.
*
* WordPress trac following the issue of escaping CSS classes:
* @link https://core.trac.wordpress.org/ticket/33924
*/
$patterns = apply_filters( 'nav_menu_css_class_unescape_patterns', '/___/');
$replacements = apply_filters( 'nav_menu_css_class_unescape_replacements', ':' );
$classes = array_map(function($cssclass) use( $patterns, $replacements) {
return preg_replace($patterns, $replacements, $cssclass);
}, $classes);
if (property_exists($args, 'a_class')) {
$arr_classes = explode(' ', $args->a_class);
$classes = array_merge($classes, $arr_classes);
}
if (property_exists($args, "a_class_$depth")) {
$arr_classes = explode(' ', $args->{"a_class_$depth"});
$classes = array_merge($classes, $arr_classes);
}
$atts['class'] = implode(' ', $classes);
return $atts;
}, 1, 4 );
/**
* Add custom classes to lis in wp_nav_menu
*/
add_filter( 'nav_menu_css_class', function ( $classes, $item, $args, $depth ) {
if (property_exists($args, 'li_class')) {
$arr_classes = explode(' ', $args->li_class);
$classes = array_merge($classes, $arr_classes);
}
if (property_exists($args, "li_class_$depth")) {
$arr_classes = explode(' ', $args->{"li_class_$depth"});
$classes = array_merge($classes, $arr_classes);
}
return $classes;
}, 1, 4 );
/**
* Add custom classes to ul.sub-menu in wp_nav_menu
*/
add_filter('nav_menu_submenu_css_class', function( $classes, $args, $depth ) {
if (property_exists($args, 'submenu_class')) {
$arr_classes = explode(' ', $args->submenu_class);
$classes = array_merge($classes, $arr_classes);
}
if (property_exists($args, "submenu_class_$depth")) {
$arr_classes = explode(' ', $args->{"submenu_class_$depth"});
$classes = array_merge($classes, $arr_classes);
}
return $classes;
}, 1, 3);
/**
* Apply our new custom attributes to widgetized wp_nav_menus
*/
add_filter('widget_nav_menu_args', function($nav_menu_args, $nav_menu, $args, $instance) {
// if($args['id'] == 'sidebar-footer') {
// $nav_menu_args['menu_class'] = 'mt-5 font-alt text-sm font-normal leading-7 opacity-60';
// $nav_menu_args['a_class'] = 'text-gray-300 opacity-60 hover:opacity-100';
// }
return $nav_menu_args;
}, 10, 4);
@jansolo76
Copy link

jansolo76 commented Aug 25, 2021

Hi David!
This is really helpful. I'm still learning when it comes to tailwind and alpine, but wouldn't it be necessary (on a mobile nav) to add custom attributes to let's say ul and li classes to control opening and closing different levels of the navigation? it would also be good to add something like a toggling container after the first level to toggle the second level.

If could give me a hand (or point me to a solution) to achieve this i would be really thankful!

Best regards
Jan

@davidwebca
Copy link
Author

davidwebca commented Aug 26, 2021

Hi @jansolo76!

Just for your info, I made this available as a package on a separate repo, in case you're using Composer. https://github.com/davidwebca/wordpress-menu-classes

So, for now, the default WordPress function only allows adding custom html attributes to links, so you can potentially use your links as refs. Since it's the lowest denominator, it's possible to do something like $refs.thelink.parentNode.parentNode, etc. and add the x-data attribute to a wrapper around your menu. Otherwise, there's a little secret that they don't make very explicit in Alpine's docs, but the data object is a callable, which means you can do something like this:

Alpine.data('menu', () => {
    // Add some initial logic here... like adding attributes to the dom! This all executes before any init functions.
    let menuElements = document.querySelectorAll('.nav li.nav-item');
    for (let i=0, il=menuElements.length; i < il; ++i) {
        let menuElement = menuElements[i]
        menuElement.setAttribute('x-on:click.prevent', "openMenu == null ? openMenu = $el.parentNode.getAttribute('id') : openMenu = null");
    }

    return {
        openMenu: null
    }
});

This is untested code, but I use a similar pattern in my projects nowadays. I initialize lots of attributes on the elements before they are actually used by Alpine internally. This is the best way to do things when you don't have full access to customize the markup.

The other thing I should mention is that there's also a package available to use to completely change the navigation markup if you use Sage as a starter theme + blade view. https://github.com/Log1x/navi

And then the other alternative is to create a custom nav walker: https://developer.wordpress.org/reference/classes/walker_nav_menu/.

Lots of options! I usually go for the first one since it's really isolating my JS, even though it feels kinda dirty to add markup attributes with JavaScript to have it interpreted after the fact... by another piece of JavaScript, buuut until WordPress gives us full control over the nav rendering, we have to dance a little!

@hofmannsven
Copy link

@davidwebca Thanks a lot! 👍

@davidwebca
Copy link
Author

@hofmannsven No worries! Don't forget to check out the package this gist has evolved into. I added a few more functionalities recently. https://github.com/davidwebca/wordpress-menu-classes

@nikolailehbrink
Copy link

@davidwebca Hi there! The script is exactly what I was looking for. Unfortunately I am quite new to wordpress theme dev. How can I activate this functionality if I dont use composer? I added the file to my theme, but that doesn't seem to change anything.

@davidwebca
Copy link
Author

@nikolailehbrink Hi! You can basically copy the whole content of the file here: https://github.com/davidwebca/wordpress-menu-classes/blob/main/src/WordPressMenuClasses.php

and add it to your functions.php (without the first line with the namespace)

@nikolailehbrink
Copy link

nikolailehbrink commented Aug 11, 2022

Thanks for the response! @davidwebca Okay, that's what I thought, but I find that a little messy. Is there a better way with separation of files?

@davidwebca
Copy link
Author

davidwebca commented Aug 11, 2022

Using composer would enable the best separation of files, but otherwise you can simply put WordPressMenuClasses.php in your theme wherever and include it from your functions.php 🤔

<?php
/**
 * At the top of your functions.php file
 */
require_once('WordPressMenuClasses.php');

@nikolailehbrink
Copy link

@davidwebca Worked fine! Thank you! :)

@davidwebca
Copy link
Author

Great glad to hear!

@Vince-ALIEN
Copy link

HI David,

It's really a great job, and it' really usefull. I hop in the future put walker's in the trash...

I'm near to have the best menu of the worrld, but I am encountering a problem : https://github.com/Vince-ALIEN/dev/blob/main/the-header-content.php.

I use Tailwind css and Alpine js.

It's a offcanvas menu on desktop and mobile, the burger open menu w-1/2 and li_atts_0 push it to w-full.

First, i would liketo close the submenu when i click on other li_atts_0 to open them, and then i want to keep the menu full width.

Does your response to @jansolo76 it's the way to realise this ?

Do you have just an idea ? It'would be great...

Encore merci.

Vincent.

@davidwebca
Copy link
Author

davidwebca commented Nov 24, 2023

Hey Vince! This gist is now a full composer package : https://github.com/davidwebca/wordpress-menu-classes

You can look at the updated file there if you want to use it standalone : https://github.com/davidwebca/wordpress-menu-classes/blob/main/src/WordPressMenuClasses.php

Especially since there were updates to WordPress core that I contributed that allows to add classes and attributes to the links directly (instead of only being able to add them to the LI elements).

What I'm looking at in your linked code seems to be fine unless I don't understand completely what you want to achieve. One thing to note is that if you want to only keep one submenu opened, you need to store an ID of some sort.

Here's a quick demo of how I do it and you can also check out the newest Alpine plugin Anchor which solves a part of the headache for anchoring submenus :

--
Edit: The demo is HTML only but gives you an idea of what would be the attributes to pass to which element in wp_nav_menu

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