wp_nav_menu()WP 3.0.0

Outputs an custom menu created in the admin-panel: Appearance > Menus.

Which navigation menu to output (there can be several of them) is specified in the theme_location parameter.

If the theme_location parameter is not specified, the menus to be output will be selected in the following order:

  1. A menu which matches the ID, tag or description passed in the menu parameter and if this menu has at least one link (one item).

  2. Otherwise, the first non-empty menu.

  3. Or, it will output the value returned by the function specified in the fallback_cb parameter (by default, the wp_page_menu function is specified there).

  4. If nothing fits, the function will output nothing.

In order for the theme to support menus, you need to enable this feature with:
add_theme_support('menus' )

Or you can register a place for menus with register_nav_menu(), then menu support by the theme will be turned on automatically.

Filters for changing menu item

Return

null|String|false. Void if 'echo' argument is true, menu output if 'echo' is false. False if there are no items or no menu was found.

Usage Template

wp_nav_menu( [
	'theme_location'  => '',
	'menu'            => '',
	'container'       => 'div',
	'container_class' => '',
	'container_id'    => '',
	'menu_class'      => 'menu',
	'menu_id'         => '',
	'echo'            => true,
	'fallback_cb'     => 'wp_page_menu',
	'before'          => '',
	'after'           => '',
	'link_before'     => '',
	'link_after'      => '',
	'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
	'depth'           => 0,
	'walker'          => '',
] );

Usage

wp_nav_menu( $args );
$args(array)
An array of menu parameters.
Default: array() (presets)

Arguments for the $args parameter

theme_location(string)
Identifier of the menu location in the template file. Identifier specified when registering the menu with register_nav_menu() function.
Default: ''
The menu you want to display. Can be: id, slug, or menu title.
Default: ''
container(string/false)
What to wrap the <ul> tag with. Can be: div or nav.
If you don't want to wrap it with anything, write false: container => false.
Default: div
container_class(string)
The value of the class="" attribute of the menu container.
*Default: menu-{menu slug}-container
container_id(string)
T he value of the id="" attribute for the menu container.
Default: ``
container_aria_label(string) (WP 5.5)
The value of the aria-label="" attribute for the menu container.
Default: ``
The class attribute value: <ul class="">.
Default value: menu
The id attribute value: <ul id="">.
Default: menu-{menu slug}
items_wrap(string)
The wrapping template for the menu items. The template must have a %3$s placeholder, the rest is optional.
Default: <ul id="%1$s" class="%2$s">%3$s</ul>
fallback_cb(string)
Function to handle output if no menu is found.
Passes all $args to the callback function.
Set '__return_empty_string' or __return_empty_string' to output nothing if no menu is found.
Default: wp_page_menu
before(string)
Text before tag <a> in the menu.
Default: ``
after(string)
Text after each tag </a> in the menu.
Default: ''
Text before the anchor of each link in the menu.
Default: ''
Text after the anchor of each link in the menu.
Default: ''
depth(number)
Up to which nesting level you want to show links (menu items). 0 - all levels.
Default: 0
item_spacing(string) (WP 4.7)
Whether or not to leave line breaks in the HTML code of the menu. Can be: preserve or discard.
Default: preserve
echo(true/false)
Display or return for processing.
Default: true
walker(object)
The class that will be used to build the menu. You must specify an object instance, not a string, such as new My_Menu_Walker(). How to use see this example.
Default: Walker_Nav_Menu()

Examples

1

#1 The first non-empty menu, with default output settings:

<?php wp_nav_menu(); ?>
0

#2 Let's display a menu called "Site Navigation":

<?php
wp_nav_menu( [
	'menu' => 'Site Navigation'
] );
?>
0

#3 Menu from pages. Example from the theme: Twenty Ten.

If no output parameters are specified and the menu is not found, the menu will be built from pages, using the function wp_page_menu().

In this example, the menu attached to the 'primary' menu area will be output:

<div id="access" role="navigation">
	<?php
	wp_nav_menu( [
		'container_class' => 'menu-header',
		'theme_location' => 'primary'
	] );
	?>
</div>
0

#4 Using the wp_nav_menu_args filter to set defaults for all menus

To remove the container for all navigation menus at once, use the following code in the functions.php theme file. Use the hook wp_nav_menu_args:

add_filter( 'wp_nav_menu_args', 'my_wp_nav_menu_args' );
function my_wp_nav_menu_args( $args = '' ){
	$args['container'] = false;

	return $args;
}
0

#5 Let's remove the container of only one rendered menu

<?php
wp_nav_menu( [
	'container' => ''
] );
?>
0

#6 Remove ul wrapper

This example will remove the <ul> tag wrapper from the menu:

<?php wp_nav_menu( [ 'items_wrap' => '%3$s' ] ); ?>
0

#7 Adding a word at the beginning of the menu

This example shows how to add a word to the beginning of a menu, in the menu item style (just not a link).

Let's add the word "List" to the beginning of the menu, also let's specify the id attribute to the created li tag:

<?php
wp_nav_menu( [
	'theme_location' => 'primary',
	'items_wrap' => '<ul><li id="item-id">List: </li>%3$s</ul>'
] );
?>
0

#8 Add CSS classes to all menus

Using the hook we can add our own CSS classes, as long as we fulfill the prerequisite.

Let's add a CSS class if it is a post and the name of the menu item is "blog":

add_filter( 'nav_menu_css_class', 'special_nav_class', 10, 2 );
function special_nav_class( $classes, $item ){

	if( is_single() && $item->title == "Blog" ){
		$classes[] = "special-class";
	}

	return $classes;
}
0

#9 Using your function to build a menu

Example of Walker_Nav_Menu class extension, to create your own custom HTML code which is outputs by function wp_nav_menu(). Our HTML code will be written specifically for our theme.

Below is the code of its own arbitrary class that builds the menu. It adds menu depth and even/odd CSS classes to menu elements (both ul and li):

To avoid reinventing the wheel, we copy the code of the Walker_Nav_Menu{} class and just modify it as we need.

// our menu building class:
class My_Walker_Nav_Menu extends Walker_Nav_Menu {

	// add classes to ul sub-menus
	function start_lvl( &$output, $depth ) {

		// depth dependent classes
		$indent = ( $depth > 0  ? str_repeat( "\t", $depth ) : '' ); // code indent
		$display_depth = ( $depth + 1); // because it counts the first submenu as 0
		$classes = array(
			'sub-menu',
			( $display_depth % 2  ? 'menu-odd' : 'menu-even' ),
			( $display_depth >=2 ? 'sub-sub-menu' : '' ),
			'menu-depth-' . $display_depth
			);
		$class_names = implode( ' ', $classes );

		// build html
		$output .= "\n" . $indent . '<ul class="' . $class_names . '">' . "\n";
	}

	// add main/sub classes to li's and links
	function start_el( &$output, $item, $depth, $args, $current_object_id = 0 ) {
		global $wp_query;

		// Restores the more descriptive, specific name for use within this method.
		$item = $data_object;

		$indent = ( $depth > 0 ? str_repeat( "\t", $depth ) : '' ); // code indent

		// depth dependent classes
		$depth_classes = array(
			( $depth == 0 ? 'main-menu-item' : 'sub-menu-item' ),
			( $depth >=2 ? 'sub-sub-menu-item' : '' ),
			( $depth % 2 ? 'menu-item-odd' : 'menu-item-even' ),
			'menu-item-depth-' . $depth
		);
		$depth_class_names = esc_attr( implode( ' ', $depth_classes ) );

		// passed classes
		$classes = empty( $item->classes ) ? array() : (array) $item->classes;
		$class_names = esc_attr( implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item ) ) );

		// build html
		$output .= $indent . '<li id="nav-menu-item-'. $item->ID . '" class="' . $depth_class_names . ' ' . $class_names . '">';

		// link attributes
		$attributes  = ! empty( $item->attr_title ) ? ' title="'  . esc_attr( $item->attr_title ) .'"' : '';
		$attributes .= ! empty( $item->target )     ? ' target="' . esc_attr( $item->target     ) .'"' : '';
		$attributes .= ! empty( $item->xfn )        ? ' rel="'    . esc_attr( $item->xfn        ) .'"' : '';
		$attributes .= ! empty( $item->url )        ? ' href="'   . esc_attr( $item->url        ) .'"' : '';
		$attributes .= ' class="menu-link ' . ( $depth > 0 ? 'sub-menu-link' : 'main-menu-link' ) . '"';

		$item_output = sprintf( '%1$s<a%2$s>%3$s%4$s%5$s</a>%6$s',
			$args->before,
			$attributes,
			$args->link_before,
			apply_filters( 'the_title', $item->title, $item->ID ),
			$args->link_after,
			$args->after
		);

		// build html
		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}
}

Now that the class is ready, use it in wp_nav_menu() function. To do this, specify an instance of our class in the walker parameter.

function my_nav_menu( $args ) {

	$args = array_merge( [
		'container'       => 'div',
		'container_id'    => 'top-navigation-primary',
		'container_class' => 'top-navigation',
		'menu_class'      => 'menu main-menu menu-depth-0 menu-even',
		'echo'            => false,
		'items_wrap'      => '<ul id="%1$s" class="%2$s">%3$s</ul>',
		'depth'           => 10,
		'walker'          => new My_Walker_Nav_Menu()
	], $args );

	echo wp_nav_menu( $args );
}

Now use our function wherever you want to display the menu:

my_nav_menu( [
	'theme_location'  => 'navigation_menu_primary'
] );

0

#10 Separate menus for (un)authorized users

If we want to show different menus, for authorized and unauthorized users, we use the conditional tag is_user_logged_in():

If ( is_user_logged_in() ) {
	 wp_nav_menu( array( 'theme_location' => 'logged-in-menu' ) );
}
else {
	 wp_nav_menu( array( 'theme_location' => 'logged-out-menu' ) );
}

You need to create 2 different menus in the admin and attach them to their respective locations (areas).

0

#11 CSS class for parent menu items

If you want to add CSS class for menu elements, which have children (stacked lists of links), then do as follows

add_filter( 'wp_nav_menu_objects', 'css_for_nav_parrent' );
function css_for_nav_parrent( $items ){

	foreach( $items as $item ){

		if( __nav_hasSub( $item->ID, $items ) ){
			// all elements of the 'classes' field of the menu, will be combined and rendered in the class attribute of the HTML tag <li>
			$item->classes[] = 'menu-parent-item';
		}
	}

	return $items;
}

function __nav_hasSub( $item_id, $items ){

	foreach( $items as $item ){

		if( $item->menu_item_parent && $item->menu_item_parent == $item_id )
			return true;
	}

	return false;
}
0

#12 Adding a class to individual menu items

There is a special hook for this: nav_menu_css_class. Classes can be added or removed through it. For an example, let's add the my__class class to all menu items:

add_filter( 'nav_menu_css_class', 'add_my_class_to_nav_menu', 10, 2 );

function add_my_class_to_nav_menu( $classes, $item ){

	/* $classes contains
	Array(
		[1] => menu-item
		[2] => menu-item-type-post_type
		[3] => menu-item-object-page
		[4] => menu-item-284
	)
	*/
	$classes[] = 'my__class';

	return $classes;
}
0

#13 Display the menu only if it exists

By default, if there is no menu, it will display the pages of the site instead. But if you want to display the menu only if it's created in the admin panel, specify the fallback_cb parameter as __return_empty_string:

wp_nav_menu( [
	'theme_location' => 'primary-menu',
	'fallback_cb' => '__return_empty_string
] );
0

#14 Output menu sub-item only

Let's say we have a first level and each of the first level items has its own submenu. We need to display such a submenu for an item with class menu-item-135:

## Cut all LI of the desired submenu and output them in our UL block
$menu = wp_nav_menu( [
	'theme_location' => 'header_menu',
	'container' => '',
	'echo' => 0,
] );

$regex_part = preg_quote( 'menu-item-135' );

// print the "gotovye-resheniya" submenu
preg_match('~'. $regex_part .'.*sub-menu[^>]+>(.*?)</ul>~s', $menu, $mm );

if( ! empty( $mm[1] ) )
	echo "<ul>$mm[1]</ul>"

It's not very optimal, but it works. It may come in handy sometimes, for little-visited sites where you need to get a quick result.

0

#15 Caching menus (wp_nav_menu) into the object cache

The code below demonstrates how to cache the entire menu code in object cache. The cache will be flushed when the menu is updated. Caching will work for all menus.

GitHub

<?php

Pj_Cached_Nav_Menus::load();

/**
 * Caches calls to wp_nav_menu().
 */
class Pj_Cached_Nav_Menus {
	public static $ttl = 3600; // use 0 to cache forever (until nav menu update)
	public static $cache_menus = array();

	public static function load() {
		add_filter( 'pre_wp_nav_menu', array( __CLASS__, 'pre_wp_nav_menu' ), 10, 2 );
		add_filter( 'wp_nav_menu', array( __CLASS__, 'maybe_cache_nav_menu' ), 10, 2 );
		add_action( 'wp_update_nav_menu', array( __CLASS__, 'clear_caches' ) );
	}

	private static function _cache_key( $args ) {
		$_args = (array) $args;
		unset( $_args['menu'] );
		return 'pj-cached-nav-menu:' . md5( json_encode( $_args ) );
	}

	private static function _timestamp() {
		static $timestamp;
		if ( ! isset( $timestamp ) )
			$timestamp = get_option( 'pj-cached-nav-menus-timestamp', 0 );

		return $timestamp;
	}

	public static function pre_wp_nav_menu( $output, $args ) {
		if ( ! empty( $args->menu ) )
			return $output;

		$cache_key = self::_cache_key( $args );
		self::$cache_menus[] = $cache_key;

		$cache = get_transient( $cache_key );
		if ( is_array( $cache ) && $cache['timestamp'] >= self::_timestamp() ) {
			$output = $cache['html'] . '<!-- pj-cached-nav-menu -->';
		}

		return $output;
	}

	public static function maybe_cache_nav_menu( $html, $args ) {
		$cache_key = self::_cache_key( $args );

		if ( ! in_array( $cache_key, self::$cache_menus ) )
			return $html;

		$cache = array(
			'html' => $html,
			'timestamp' => time(),
		);

		set_transient( $cache_key, $cache, self::$ttl );
		return $html;
	}

	public static function clear_caches() {
		update_option( 'pj-cached-nav-menus-timestamp', time() );
	}
}

Original code: https://github.com/pressjitsu/cached-nav-menus

Composer package: https://github.com/inpsyde/menu-cache

Another similar example: https://www.bjornjohansen.com/wordpress-menu-cache

CSS classes for menu items

The following CSS classes are added to menu items (separating by condition which pages the user is on):

For all elements on all pages

  • .menu-item ‒ for all menu items;

  • .menu-item-object-{object} ‒ for all items where {object} is replaced by post type or taxonomy name:

    • .menu-item-object-category ‒ for categories.
    • .menu-item-object-tag ‒ for tags.
    • .menu-item-object-page ‒ for permanent pages.
    • .menu-item-object-custom ‒ for an arbitrary menu item.
  • .menu-item-type-{type} ‒ to all menu items, where {type} is replaced by the link type (post or taxonomy). Groups all link types:
    • .menu-item-type-post_type ‒ static page, arbitrary entry type.
    • .menu-item-type-taxonomy ‒ category, label, or arbitrary taxonomy.
    • .menu-item-type-custom ‒ for an arbitrary menu item.

For elements of the current page

  • .current-menu-item ‒ if the link in the menu coincides with the address of the viewed page. Current page.

For elements parent to the viewed page

  • .current-menu-parent
  • .current-{object}-ancestor
  • .current-{type}-ancestor
  • .current-menu-ancestor
  • .current-{object}-ancestor
  • .current-{type}-ancestor
  • .menu-item-home

Compatible with wp_page_menu()

  • .page_item
  • .page-item-$ID
  • .current_page_item
  • .current_page_parent
  • .current_page_ancestor

Rest

  • .menu-item-has-children - If the menu item has children.
  • ul.sub-menu - The child ul element.
  • .menu-item-privacy-policy - If menu item is set as the 'Privacy Policy Page'.
  • .menu-item-home - If menu item is set as the 'Front Page'.

Object $item

$item parameters

The $item is often used in the examples. Almost all of the parameters of this item are shown below:

Field Description
ID menu item ID
menu_item_parent the ID of the parent menu item
classes array of menu item classes
post_date date of addition
post_modified date of last modification
post_author ID of the user who added the menu item
title title of the menu item
url menu item link
attr_title title attribute of the link
xfn rel link attribute
target target attribute link
current equal to 1 if it is a current item
current_item_ancestor 1 if the current item is a nested item
current_item_parent 1 if the current item is a parent item
menu_order sequence number in the menu
object_id the ID of the menu item. Entry, term, etc.
type type of menu object (taxa, record)
object name of taxa, record type: page, category, post_tag ...
type_label localized name of type: Heading, Page
post_parent the ID of the parent record
post_title title of the record
post_name record label
Example $item object
WP_Post Object (
	[ID]                    => 10
	[post_author]           => 5
	[post_date]             => 2019-02-11 13:33:39
	[post_date_gmt]         => 2019-02-11 13:33:39
	[post_content]          =>
	[post_title]            => New
	[post_excerpt]          =>
	[post_status]           => publish
	[comment_status]        => closed
	[ping_status]           => closed
	[post_password]         =>
	[post_name]             => new
	[to_ping]               =>
	[pinged]                =>
	[post_modified]         => 2019-02-11 23:10:19
	[post_modified_gmt]     => 2019-02-11 23:10:19
	[post_content_filtered] =>
	[post_parent]           => 0
	[guid]                  => http://dh5.com/?p=10
	[menu_order]            => 1
	[post_type]             => nav_menu_item
	[post_mime_type]        =>
	[comment_count]         => 0
	[filter]                => raw
	[db_id]                 => 10
	[menu_item_parent]      => 0
	[object_id]             => 10
	[object]                => custom
	[type]                  => custom
	[type_label]            => Custom Link
	[title]                 => New
	[url]                   => #
	[target]                =>
	[attr_title]            =>
	[description]           =>
	[classes]               => Array
		[0] => extra-sub-menu
		[1] => menu-item
		[2] => menu-item-type-custom
		[3] => menu-item-object-custom
	[xfn]                   =>
	[current]               =>
	[current_item_ancestor] =>
	[current_item_parent]   =>
)

Example of using the walker parameter

In walker you can specify the object that will build the menu. In this object, you can describe the HTML code of the resulting menu.

If you want to create a menu for a non-standard HTML layout, it is sometimes easier to redo this object than to redo the HTML layout.

As an example of a walker object, let's take the Walker_Nav_Menu{} class, which is used by default. In it, we are interested only in one method start_el(). It is this one which is responsible for the HTML of each element. As a rule, it is enough to change only it.

To do this, we need to create our own class that will extend the Walker_Nav_Menu class and specify it in the walker parameter when the menu is called.

Let's look at an example. We take the code of the start_el() method without any changes. Use it as a template:

class My_Walker_Nav_Menu extends Walker_Nav_Menu {

	/**
	 * Starts the element output.
	 *
	 * @since 3.0.0
	 * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
	 *
	 * @see Walker::start_el()
	 *
	 * @param string   $output Passed by reference. Used to append additional content.
	 * @param WP_Post  $item   Menu item data object.
	 * @param int      $depth  Depth of menu item. Used for padding.
	 * @param stdClass $args   An object of wp_nav_menu() arguments.
	 * @param int      $id     Current item ID.
	 */
	public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {

		if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
			$t = '';
			$n = '';
		} else {
			$t = "\t";
			$n = "\n";
		}
		$indent = ( $depth ) ? str_repeat( $t, $depth ) : '';

		$classes = empty( $item->classes ) ? array() : (array) $item->classes;
		$classes[] = 'menu-item-' . $item->ID;

		$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );

		$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
		$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';

		$id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
		$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';

		// создаем HTML код элемента меню
		$output .= $indent . '<li' . $id . $class_names .'>';

		$atts = array();
		$atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
		$atts['target'] = ! empty( $item->target )     ? $item->target     : '';
		$atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
		$atts['href']   = ! empty( $item->url )        ? $item->url        : '';

		$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );

		$attributes = '';
		foreach ( $atts as $attr => $value ) {
			if ( ! empty( $value ) ) {
				$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
				$attributes .= ' ' . $attr . '="' . $value . '"';
			}
		}

		$title = apply_filters( 'the_title', $item->title, $item->ID );
		$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );

		$item_output = $args->before;
		$item_output .= '<a'. $attributes .'>';
		$item_output .= $args->link_before . $title . $args->link_after;
		$item_output .= '</a>';
		$item_output .= $args->after;

		$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
	}

}

Now, when you call up the menu, you specify your walker:

wp_nav_menu( [
	'theme_location' => 'head_menu',
	'walker'         => new My_Walker_Nav_Menu(),
] );

Done, now each menu item will be built according to the desired HTML scheme.

BEM menu with filters

This BEM methodology will be applied to the HTML layout:

<ul class="menu menu--main menu--horizontal">
	<li class="menu-node menu-node--main_lvl_1 menu-node--active">
		<a href="#" class="menu-link menu-link--active">Пункт 1</a>
		<ul class="menu menu--dropdown menu--vertical">
			<li class="menu-node menu-node--main_lvl_2">
				<a href="#" class="menu-link">Подпункт 1.1</a>
			</li>
			<li class="menu-node menu-node--main_lvl_2">
				<a href="#" class="menu-link">Подпункт 1.2</a>
			</li>
		</ul>
	</li>
	<li class="menu-node menu-node--main_lvl_1">
		<a href="#" class="menu-link">Пункт 2</a>
	</li>
	<li class="menu-node menu-node--main_lvl_1">
		<a href="#" class="menu-link">Пункт 3</a>
	</li>
</ul>

index.php file or other file to display the menu

<?php
wp_nav_menu( [
	'theme_location' => 'header-menu',
] );

functions.php file

<?php
add_action( 'after_setup_theme', function () {
	register_nav_menus( [
		'header-menu' => 'Top area',
		'footer-menu' => 'Bottom area',
	] );
} );

// Changes the basic parameters of the menu
add_filter( 'wp_nav_menu_args', 'filter_wp_menu_args' );

// Change the id attribute of the li tag
add_filter( 'nav_menu_item_id', 'filter_menu_item_css_id', 10, 4 );

// Change the class attribute of the li tag
add_filter( 'nav_menu_css_class', 'filter_nav_menu_css_classes', 10, 4 );

// Changes the class of the nested ul
add_filter( 'nav_menu_submenu_css_class', 'filter_nav_menu_submenu_css_class', 10, 3 );

// Add classes to links
add_filter( 'nav_menu_link_attributes', 'filter_nav_menu_link_attributes', 10, 4 );

function filter_wp_menu_args( $args ) {
	if ( $args['theme_location'] === 'header-menu' ) {
		$args['container']  = false;
		$args['items_wrap'] = '<ul class="%2$s">%3$s</ul>';
		$args['menu_class'] = 'menu menu--main menu-horizontal';
	}
	return $args;
}

function filter_menu_item_css_id( $menu_id, $item, $args, $depth ) {
	return $args->theme_location === 'header-menu' ? '' : $menu_id;
}

function filter_nav_menu_css_classes( $classes, $item, $args, $depth ) {
	if ( $args->theme_location === 'header-menu' ) {
		$classes = [
			'menu-node',
			'menu-node--main_lvl_' . ( $depth + 1 )
		];
		if ( $item->current ) {
			$classes[] = 'menu-node--active';
		}
	}
	return $classes;
}

function filter_nav_menu_submenu_css_class( $classes, $args, $depth ) {
	if ( $args->theme_location === 'header-menu' ) {
		$classes = [
			'menu',
			'menu--dropdown',
			'menu--vertical'
		];
	}
	return $classes;
}

function filter_nav_menu_link_attributes( $atts, $item, $args, $depth ) {
	if ( $args->theme_location === 'header-menu' ) {
		$atts['class'] = 'menu-link';
		if ( $item->current ) {
			$atts['class'] .= ' menu-link--active';
		}
	}
	return $atts;
}

Changelog

Since 3.0.0 Introduced.
Since 4.7.0 Added the item_spacing argument.
Since 5.5.0 Added the container_aria_label argument.

wp_nav_menu() code WP 6.4.3

function wp_nav_menu( $args = array() ) {
	static $menu_id_slugs = array();

	$defaults = array(
		'menu'                 => '',
		'container'            => 'div',
		'container_class'      => '',
		'container_id'         => '',
		'container_aria_label' => '',
		'menu_class'           => 'menu',
		'menu_id'              => '',
		'echo'                 => true,
		'fallback_cb'          => 'wp_page_menu',
		'before'               => '',
		'after'                => '',
		'link_before'          => '',
		'link_after'           => '',
		'items_wrap'           => '<ul id="%1$s" class="%2$s">%3$s</ul>',
		'item_spacing'         => 'preserve',
		'depth'                => 0,
		'walker'               => '',
		'theme_location'       => '',
	);

	$args = wp_parse_args( $args, $defaults );

	if ( ! in_array( $args['item_spacing'], array( 'preserve', 'discard' ), true ) ) {
		// Invalid value, fall back to default.
		$args['item_spacing'] = $defaults['item_spacing'];
	}

	/**
	 * Filters the arguments used to display a navigation menu.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param array $args Array of wp_nav_menu() arguments.
	 */
	$args = apply_filters( 'wp_nav_menu_args', $args );
	$args = (object) $args;

	/**
	 * Filters whether to short-circuit the wp_nav_menu() output.
	 *
	 * Returning a non-null value from the filter will short-circuit wp_nav_menu(),
	 * echoing that value if $args->echo is true, returning that value otherwise.
	 *
	 * @since 3.9.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string|null $output Nav menu output to short-circuit with. Default null.
	 * @param stdClass    $args   An object containing wp_nav_menu() arguments.
	 */
	$nav_menu = apply_filters( 'pre_wp_nav_menu', null, $args );

	if ( null !== $nav_menu ) {
		if ( $args->echo ) {
			echo $nav_menu;
			return;
		}

		return $nav_menu;
	}

	// Get the nav menu based on the requested menu.
	$menu = wp_get_nav_menu_object( $args->menu );

	// Get the nav menu based on the theme_location.
	$locations = get_nav_menu_locations();
	if ( ! $menu && $args->theme_location && $locations && isset( $locations[ $args->theme_location ] ) ) {
		$menu = wp_get_nav_menu_object( $locations[ $args->theme_location ] );
	}

	// Get the first menu that has items if we still can't find a menu.
	if ( ! $menu && ! $args->theme_location ) {
		$menus = wp_get_nav_menus();
		foreach ( $menus as $menu_maybe ) {
			$menu_items = wp_get_nav_menu_items( $menu_maybe->term_id, array( 'update_post_term_cache' => false ) );
			if ( $menu_items ) {
				$menu = $menu_maybe;
				break;
			}
		}
	}

	if ( empty( $args->menu ) ) {
		$args->menu = $menu;
	}

	// If the menu exists, get its items.
	if ( $menu && ! is_wp_error( $menu ) && ! isset( $menu_items ) ) {
		$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
	}

	/*
	 * If no menu was found:
	 *  - Fall back (if one was specified), or bail.
	 *
	 * If no menu items were found:
	 *  - Fall back, but only if no theme location was specified.
	 *  - Otherwise, bail.
	 */
	if ( ( ! $menu || is_wp_error( $menu ) || ( isset( $menu_items ) && empty( $menu_items ) && ! $args->theme_location ) )
		&& isset( $args->fallback_cb ) && $args->fallback_cb && is_callable( $args->fallback_cb ) ) {
			return call_user_func( $args->fallback_cb, (array) $args );
	}

	if ( ! $menu || is_wp_error( $menu ) ) {
		return false;
	}

	$nav_menu = '';
	$items    = '';

	$show_container = false;
	if ( $args->container ) {
		/**
		 * Filters the list of HTML tags that are valid for use as menu containers.
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $tags The acceptable HTML tags for use as menu containers.
		 *                       Default is array containing 'div' and 'nav'.
		 */
		$allowed_tags = apply_filters( 'wp_nav_menu_container_allowedtags', array( 'div', 'nav' ) );

		if ( is_string( $args->container ) && in_array( $args->container, $allowed_tags, true ) ) {
			$show_container = true;
			$class          = $args->container_class ? ' class="' . esc_attr( $args->container_class ) . '"' : ' class="menu-' . $menu->slug . '-container"';
			$id             = $args->container_id ? ' id="' . esc_attr( $args->container_id ) . '"' : '';
			$aria_label     = ( 'nav' === $args->container && $args->container_aria_label ) ? ' aria-label="' . esc_attr( $args->container_aria_label ) . '"' : '';
			$nav_menu      .= '<' . $args->container . $id . $class . $aria_label . '>';
		}
	}

	// Set up the $menu_item variables.
	_wp_menu_item_classes_by_context( $menu_items );

	$sorted_menu_items        = array();
	$menu_items_with_children = array();
	foreach ( (array) $menu_items as $menu_item ) {
		/*
		 * Fix invalid `menu_item_parent`. See: https://core.trac.wordpress.org/ticket/56926.
		 * Compare as strings. Plugins may change the ID to a string.
		 */
		if ( (string) $menu_item->ID === (string) $menu_item->menu_item_parent ) {
			$menu_item->menu_item_parent = 0;
		}

		$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
		if ( $menu_item->menu_item_parent ) {
			$menu_items_with_children[ $menu_item->menu_item_parent ] = true;
		}
	}

	// Add the menu-item-has-children class where applicable.
	if ( $menu_items_with_children ) {
		foreach ( $sorted_menu_items as &$menu_item ) {
			if ( isset( $menu_items_with_children[ $menu_item->ID ] ) ) {
				$menu_item->classes[] = 'menu-item-has-children';
			}
		}
	}

	unset( $menu_items, $menu_item );

	/**
	 * Filters the sorted list of menu item objects before generating the menu's HTML.
	 *
	 * @since 3.1.0
	 *
	 * @param array    $sorted_menu_items The menu items, sorted by each menu item's menu order.
	 * @param stdClass $args              An object containing wp_nav_menu() arguments.
	 */
	$sorted_menu_items = apply_filters( 'wp_nav_menu_objects', $sorted_menu_items, $args );

	$items .= walk_nav_menu_tree( $sorted_menu_items, $args->depth, $args );
	unset( $sorted_menu_items );

	// Attributes.
	if ( ! empty( $args->menu_id ) ) {
		$wrap_id = $args->menu_id;
	} else {
		$wrap_id = 'menu-' . $menu->slug;

		while ( in_array( $wrap_id, $menu_id_slugs, true ) ) {
			if ( preg_match( '#-(\d+)$#', $wrap_id, $matches ) ) {
				$wrap_id = preg_replace( '#-(\d+)$#', '-' . ++$matches[1], $wrap_id );
			} else {
				$wrap_id = $wrap_id . '-1';
			}
		}
	}
	$menu_id_slugs[] = $wrap_id;

	$wrap_class = $args->menu_class ? $args->menu_class : '';

	/**
	 * Filters the HTML list content for navigation menus.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $items The HTML list content for the menu items.
	 * @param stdClass $args  An object containing wp_nav_menu() arguments.
	 */
	$items = apply_filters( 'wp_nav_menu_items', $items, $args );
	/**
	 * Filters the HTML list content for a specific navigation menu.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $items The HTML list content for the menu items.
	 * @param stdClass $args  An object containing wp_nav_menu() arguments.
	 */
	$items = apply_filters( "wp_nav_menu_{$menu->slug}_items", $items, $args );

	// Don't print any markup if there are no items at this point.
	if ( empty( $items ) ) {
		return false;
	}

	$nav_menu .= sprintf( $args->items_wrap, esc_attr( $wrap_id ), esc_attr( $wrap_class ), $items );
	unset( $items );

	if ( $show_container ) {
		$nav_menu .= '</' . $args->container . '>';
	}

	/**
	 * Filters the HTML content for navigation menus.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $nav_menu The HTML content for the navigation menu.
	 * @param stdClass $args     An object containing wp_nav_menu() arguments.
	 */
	$nav_menu = apply_filters( 'wp_nav_menu', $nav_menu, $args );

	if ( $args->echo ) {
		echo $nav_menu;
	} else {
		return $nav_menu;
	}
}