Custom Class to wp nav menu

This is an old question, but might be very much relevant still today so I thought I would drop a piece of code I developed over the years. It’s basically as close as you can get to customizing classes on every element of a menu in WordPress without developing a custom Nav Walker. This is getting more and more relevant with utility-first CSS framweworks like Tailwind that really require you to have access to the markup or options to add your own classes.

It uses most of the filters available in the WordPress wp_nav_menu function and lifecycle to add classes everywhere. First, you use wp_nav_menu as you would usually, but then pass custom parameters to the function. Those custom parameters are then detected by the filters and add the classes where they need to be.

One would be able, for example, to create a custom dropdown menu by adding only classes to the first level of list elements or add a background image / icon to only 3rd level links. All the possible options are detailed in the docblock at the top.

<?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);