wp_nav_menu()WP 3.0.0

Outputs the menu created in the admin panel: "Appearance > Menus".

Which specific menu to display (there can be several) is specified in the parameter theme_location or menu.

To enable menu support in the theme, this feature must be activated using:
add_theme_support( 'menus' )

Alternatively, you can register a location (slot) for the menu using register_nav_menu(), which will automatically enable menu support for the theme.

Important: nuance with a missing menu

When using the function, if the specified theme_location does not exist or no menu is assigned to it, WordPress will pass control to the function wp_page_menu(), which outputs a list of all pages instead of the menu. In this case, the parameters container, container_class, and others will start applying to <ul>, not to the container, which may disrupt the expected HTML markup.

Example:

$menu = wp_nav_menu( [
	'theme_location'  => 'my_location',

	'container'       => 'nav',
	'container_class' => 'nav header__nav',
	'menu_class'      => 'header__list',
	'menu_id'         => 'my-id',

	'echo' => false,
] );

echo htmlspecialchars( $menu );

Result when "my_location" is missing or has no menu:

<nav id="my-id" class="header__list">
	<ul>
		<li class="page_item page-item-14813"><a href="URL">Page Title</a></li>
	</ul>
</nav>

Result when "my_location" has a menu:

<nav class="nav header__nav">
	<ul id="my-id" class="header__list">
		<li id="menu-item-6411" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-6411">
			<a href="URL">Menu Item Title</a>
		</li>
	</ul>
</nav>

🛑 This behavior breaks the markup if you expect that menu_class will be applied to <ul>, not to <nav>.

✅ To avoid such situations:

  • Ensure that the required theme_location is registered via register_nav_menus().
  • Assign at least one menu to the specified theme_location slot in the admin panel.
  • Or explicitly specify menu by name if you want to avoid fallback to wp_page_menu().

🛑 Always check that the menu exists, especially during layout and styling.

For more information on enabling and adding menus, read in a separate article.

Filters for modifying menu items

Returns

null|String|false. The function outputs the HTML code for the menu.

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

<?php wp_nav_menu( $args ); ?>
$args(array)
Array of parameters for the displayed menu.
Default: array() (defaults)

Arguments for the $args parameter

theme_location(string)

Identifier for the menu location in the template. The identifier is specified when registering the menu using the function register_nav_menu().

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

  1. A menu that matches the ID, slug, or description passed in the menu parameter and if this menu has at least one link (one item).
  2. Otherwise, the first menu from a non-empty slot for the menu.
  3. Or, it will output the value returned by the function specified in the fallback_cb parameter. By default, this is the function wp_page_menu().
  4. If nothing matches, the function will output nothing.

Default: ''

The menu to be displayed. You can specify: id, slug, or menu name.
Default: ''
container(string/false)
What to wrap the ul tag with. Can be: div or nav.
If you do not want to wrap it with anything, write false: container => false.
Default: div
container_class(string)
Value of the class="" attribute for the menu container.
Default: menu-{menu slug}-container
container_id(string)
Value of the id="" attribute for the menu container.
Default: ''
container_aria_label(string) (WP 5.5)
Value of the aria-label="" attribute for the menu container.
Default: ''
Value of the class attribute for the ul tag.
Default: menu
Value of the id attribute for the ul tag.
Default: menu-{menu slug}
items_wrap(string)
Template wrapper for menu items. The template must have the placeholder %3$s, the rest is optional.
Default: '<ul id="%1$s" class="%2$s">%3$s</ul>'
fallback_cb(string)
The function to handle output if no menu is found.
Passes all $args arguments to the specified function.
Set an empty string '' or '__return_empty_string' to output nothing if the menu is absent.
Default: wp_page_menu
before(string)
Text before the <a> tag in the menu.
Default: ''
after(string)
Text after each </a> tag 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(int)
Up to what level of nesting to show links (menu items). 0 - all levels.
Default: 0
item_spacing(string) (WP 4.7)
Whether to keep line breaks in the HTML code of the menu. Can be: preserve or discard
Default: 'preserve'
echo(true/false)
Output to the screen or return for processing.
Default: true
walker(object)

Class that will be used to build the menu. You need to specify an instance of the object, not a string, for example new My_Menu_Walker().

For usage see this example.
Default: Walker_Nav_Menu()

Examples

4

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

<?php wp_nav_menu(); ?>
1

#2 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

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

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

#4 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

#5 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

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

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

#7 Remove ul wrapper

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

<?php wp_nav_menu( [ 'items_wrap' => '%3$s' ] ); ?>
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.

/**
 * Custom walker class for nav menus.
 */
class My_Walker_Nav_Menu extends Walker_Nav_Menu {

	/**
	 * Adds classes to ul sub-menus.
	 *
	 * @return void
	 */
	function start_lvl( &$output, $depth = 0, $args = null ) {
		// 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 = [
			'sub-menu',
			( $display_depth % 2 ? 'menu-odd' : 'menu-even' ),
			( $display_depth >= 2 ? 'sub-sub-menu' : '' ),
			'menu-depth-' . $display_depth,
		];
		$class_names = implode( ' ', $classes );

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

	//

	/**
	 * Adds main classes to li's and links.
	 *
	 * @return void
	 */
	function start_el( &$output, $data_object, $depth, $args, $current_object_id = 0 ) {
		$item = $data_object; // use more descriptive name for use within this method.

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

		// depth dependent classes
		$depth_classes = [
			( $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) $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 = strtr( '{BEFORE}<a{ATTRIBUTES}>{LINK_BEFORE}{TITLE}{LINK_AFTER}</a>{AFTER}', [
			'{BEFORE}'      => $args->before,
			'{ATTRIBUTES}'  => $attributes,
			'{LINK_BEFORE}' => $args->link_before,
			'{TITLE}'       => apply_filters( 'the_title', $item->title, $item->ID ),
			'{LINK_AFTER}'  => $args->link_after,
			'{AFTER}'       => $args->after,
		] );

		$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

See the added classes in the function _wp_menu_item_classes_by_context().

The following CSS classes are added to menu items (divided by conditions on which pages the user is located):

For all items on all pages

  • .menu-item — for all menu items;

  • .menu-item-object-{object} — for all items. {object} will be replaced with the name of the post type or taxonomy:

    • .menu-item-object-category — for categories.
    • .menu-item-object-tag — for tags.
    • .menu-item-object-page — for static pages.
    • .menu-item-object-custom ‒ for custom menu items.
  • .menu-item-type-{type} — for all menu items. {type} will be replaced with the type of link (post or taxonomy):
    • .menu-item-type-post_type — static page, custom post type.
    • .menu-item-type-taxonomy — category, tag, or custom taxonomy.
    • .menu-item-type-custom ‒ for custom menu items.

On the current page

  • .current-menu-item — if the link in the menu matches the address of the viewed page. The current page.

For parent items of the viewed page

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

Compatibility with the function wp_page_menu()

  • .page_item
  • .page-item-{$menu_item->object_id}
  • .current_page_item
  • .current_page_parent
  • .current_page_ancestor

Others

  • .menu-item-has-children ‒ If the item has child elements.
  • ul.sub-menu ‒ For the child element ul.
  • .menu-item-privacy-policy ‒ menu item "Privacy Policy Page".
  • .menu-item-home ‒ menu item "Home Page".

The $item object

$item parameters

In examples, the menu item $item is often used. Below are shown almost all parameters of this item:

Field Description
ID ID of the menu item
menu_item_parent ID of the parent menu item
classes array of classes for the menu item
post_date date of addition
post_modified date of last modification
post_author ID of the user who added this menu item
title title of the menu item
url link of the menu item
attr_title title attribute of the link
xfn rel attribute of the link
target target attribute of the link
current equals 1 if this is the current item
current_item_ancestor 1 if the current item is a child item
current_item_parent 1 if the current item is a parent item
menu_order order number in the menu
object_id ID of the menu object. Post, term, etc.
type type of the menu object (taxonomy, post)
object name of the taxonomy, post type: page, category, post_tag ...
type_label localized name of the type: Category, Page
post_parent ID of the parent post
post_title title of the post
post_name slug of the post
Example of the $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 the walker, you can specify an object that will build the menu. In this object, you can describe the HTML code of the resulting menu.

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

As an example of the walker object, we take the class Walker_Nav_Menu{}, which is used by default. We are only interested in one method start_el(). It is responsible for the HTML of each item. Usually, it is enough to change only this. To do this, you need to create your own class that will extend the Walker_Nav_Menu class and specify it in the walker parameter when calling the menu.

Let's look at an example. The code of the start_el() method is taken without changes. We 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 ) . '"' : '';

		// create the HTML code for the menu item
		$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 calling the menu, we specify our walker:

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

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

BEM menu using filters

The layout will be formed according to the BEM methodology:

<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">Item 1</a>
		<ul class="menu menu--dropdown menu--vertical">
			<li class="menu-node menu-node--main_lvl_2">
				<a href="#" class="menu-link">Subitem 1.1</a>
			</li>
			<li class="menu-node menu-node--main_lvl_2">
				<a href="#" class="menu-link">Subitem 1.2</a>
			</li>
		</ul>
	</li>
	<li class="menu-node menu-node--main_lvl_1">
		<a href="#" class="menu-link">Item 2</a>
	</li>
	<li class="menu-node menu-node--main_lvl_1">
		<a href="#" class="menu-link">Item 3</a>
	</li>
</ul>

File index.php or another for outputting the menu

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

File functions.php

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

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

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

// Changes 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 );

// Adds 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.9

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;
	}
}
2 comments