WordPress register nav menu PHP – Custom Navigation
Register a custom menu location with register_nav_menus(), then render it in your template with wp_nav_menu(). Includes fallback handling and a full production header nav.
Register a nav menu location in functions.php, then render it in any template with wp_nav_menu().
Use this whenever you need a custom header, footer, or sidebar navigation that editors can manage from Appearance → Menus.
Building it step by step
Register the location in functions.php first, then build the template output in 4 steps.
We’ll build this in 4 steps. Step 1 registers the menu location in functions.php. Steps 2–4 build the template output progressively — each one works on its own.
Step 1: Register the menu location. Before WordPress will let editors assign a menu, you need to declare the location in functions.php. The key you use here (primary) is what you’ll pass to wp_nav_menu() in every template that renders it.
<?php
add_action( 'after_setup_theme', function() {
register_nav_menus( array(
'primary' => 'Primary Navigation',
'footer' => 'Footer Navigation',
) );
} );
Step 2: The bare call. wp_nav_menu() with just theme_location is enough to render the assigned menu. WordPress outputs a <div> wrapper and a <ul> by default.
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
) );
Step 3: Control the container and class. By default WordPress wraps the menu in a <div class="menu-...">. Replace it with a <nav> for semantic HTML, and set the classes you need for your CSS.
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'site-nav',
'menu_class' => 'site-nav__list',
'depth' => 2,
) );
Step 4: Add a fallback. If no menu has been assigned to the location yet, WordPress falls back to listing all pages — which is almost never what you want. Set fallback_cb to false to output nothing instead of a default page list.
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'site-nav',
'menu_class' => 'site-nav__list',
'depth' => 2,
'fallback_cb' => false,
) );
Step 5: Everything together. Steps 2–4 are teaching code. This is the production header — logo, primary nav, and a mobile toggle button, all in one file.
<?php
/**
* Full file: template-parts/header/site-header.php
* Steps 2–4 combined — copy this into your theme as the production version.
*/
?>
<header class="site-header">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo">
<?php bloginfo( 'name' ); ?>
</a>
<?php
// ── Primary navigation ────────────────────────────────────────────────────
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'site-nav',
'menu_class' => 'site-nav__list',
'depth' => 2,
'fallback_cb' => false,
) );
?>
<button class="site-header__menu-toggle" aria-expanded="false" aria-controls="site-nav">
Menu
</button>
</header>
Include this file in your header.php or wherever you want the nav to appear: get_template_part( 'template-parts/header/site-header' );
How it works
One explanation per build step — what problem each layer solved.
Step 1 — register_nav_menus()
Problem solved: tells WordPress this theme supports named menu locations. Without register_nav_menus(), the Appearance → Menus screen shows no locations and editors have nowhere to assign a menu. The array key ('primary') is the internal slug you use in all PHP templates. The value ('Primary Navigation') is the human-readable label editors see in the admin.
Step 2 — The bare wp_nav_menu() call
Problem solved: outputs the menu assigned to the registered location. theme_location links this call to the slug you registered in Step 1. WordPress queries the database for the menu assigned to that location and renders it as an HTML list. No menu assigned yet? The default fallback renders a list of all pages — which is why Step 4 suppresses it.
Step 3 — Container and classes
Problem solved: replaces the default <div> wrapper with a <nav> element for semantic HTML. container_class sets the class on the wrapper element; menu_class sets the class on the inner <ul> — that’s the list your CSS typically targets. depth => 2 allows one level of dropdown children; set to 0 for unlimited depth or 1 for flat menus only.
Step 4 — Fallback
Problem solved: prevents WordPress from outputting an auto-generated page list when no menu is assigned. By default fallback_cb is set to 'wp_page_menu', which generates a list of all pages. On a fresh install this produces unexpected markup. Setting it to false outputs nothing at all until an editor assigns a real menu — much cleaner during development.
Setup
Add functions.php code, create the template file, and assign a menu in the admin.
Add register_nav_menus() to functions.php
Open your theme’s functions.php and paste the Step 1 code. You can add as many locations as you need — header primary, header secondary, footer, mobile, sidebar. Each key must be unique within the theme. Save the file.
Create the template file
Create template-parts/header/site-header.php and paste in the Step 5 production code. Then call it from your header.php:
<?php get_template_part( 'template-parts/header/site-header' ); ?>
Assign a menu and test
Go to Appearance → Menus. Create a new menu, add a few pages to it, scroll to Menu Settings → Display location, tick Primary Navigation, and click Save Menu. Reload the front end — the menu should now render inside your <nav class="site-nav"> element. If nothing shows, confirm the location key in PHP matches what you registered exactly.
The field names in the PHP must match ACF exactly — and the same rule applies here: the theme_location string in wp_nav_menu() must match the array key you passed to register_nav_menus() exactly. A typo means the menu never renders.
Making it your own
Multiple locations, accessibility labels, and conditional rendering.
Register multiple locations
Your theme can register as many locations as it needs. Common additions are footer for a slimmer footer nav, mobile for a separate mobile-only menu, and secondary for a utility nav in the header. Each location is assigned independently in the Menus admin — editors can assign different menus to each one.
Add aria-label for accessibility
When a page has more than one <nav> element, screen readers need labels to distinguish them. Pass a custom container attribute to wp_nav_menu() using the items_wrap argument, or add the aria-label directly via CSS/JS after render. Alternatively, wrap the wp_nav_menu() call in a <nav aria-label="Primary"> and set container => false to skip the auto-generated wrapper:
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'site-nav__list',
'fallback_cb' => false,
) );
Check if a menu is assigned before rendering
Use has_nav_menu() to conditionally show a section only when a menu is actually assigned:
if ( has_nav_menu( 'primary' ) ) {
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'fallback_cb' => false,
) );
}
Useful for footer menus that are optional — no empty markup is rendered on themes that don’t use the location.
Do not call register_nav_menus() directly in your theme file — always hook it to after_setup_theme. WordPress needs the action to have run before it can detect supported features. Calling it outside the hook can cause the location to not appear in the Menus admin on first load.
No comments yet. Be the first.