Tips・The foundations of WordPress customization

Making navigation menus editable from the admin screen

The steps to make a header menu editable from the admin screen. Covers how to write register_nav_menus and wp_nav_menu, removing the extra div, CSS for highlighting the current page, and building dropdowns.

“Make it so the client can change the menu items themselves”—that requirement is met with two functions: register_nav_menus() and wp_nav_menu(). It takes three steps.

Step 1: register the menu location in your theme

function my_register_menus() {
  register_nav_menus([
    'global' => 'Header menu',
    'footer' => 'Footer menu',
  ]);
}
add_action('after_setup_theme', 'my_register_menus');

The key (global) is the name you call from templates; the value is the label shown in the admin screen.

Step 2: call it in a template

<?php
wp_nav_menu([
  'theme_location' => 'global',
  'container'      => false,                          // don't output a wrapping div
  'items_wrap'     => '<ul class="global-nav">%3$s</ul>',
  'depth'          => 2,
]);
?>

Step 3: create the menu in the admin screen

  1. Open “Appearance” → “Menus
  2. “Create a new menu” → enter a name → “Create Menu”
  3. Add pages, categories and custom links from the left and drag them into order
  4. Under “Menu Settings” → “Display location,” tick “Header menu”
  5. “Save Menu”

Cheat sheet of arguments

ArgumentRoleExample
theme_locationWhich location’s menu'global'
containerThe wrapping elementfalse (none) / 'nav'
container_classClass on the wrapper'nav-wrap'
menu_classClass on the ul'global-nav'
items_wrapMarkup for the ul portion'<ul class="x">%3$s</ul>'
depthHow many levels to output1 (no children) / 2
fallback_cbFallback when no menu is setfalse (output nothing)
walkerFine-grained control of outputnew My_Walker()

Using the classes WordPress adds

WordPress adds classes describing state to each li. You can build behavior with CSS alone.

ClassWhen it’s added
current-menu-itemThe item for the page currently displayed
current-menu-parent / current-menu-ancestorIts parent items
menu-item-has-childrenAn item that has a submenu
menu-item-{ID}A unique class per item
.global-nav { display: flex; gap: 1.5rem; list-style: none; }
.global-nav a { text-decoration: none; padding: 0.5em 0; display: inline-block; }
.global-nav .current-menu-item > a { border-bottom: 2px solid currentColor; font-weight: 700; }

Dragging an item to the right in the admin screen makes it a child. You can turn that into a dropdown with CSS alone.

.global-nav .menu-item-has-children { position: relative; }
.global-nav .sub-menu {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 12em;
  list-style: none;
  background: #fff;
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
  border-radius: 8px;
  padding: 0.5rem 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease;
}
.global-nav .menu-item-has-children:hover .sub-menu,
.global-nav .menu-item-has-children:focus-within .sub-menu {
  opacity: 1;
  visibility: visible;
}

Including :focus-within means it opens for keyboard users too. Not assuming a mouse is the considerate way to build it.

A hamburger menu for phones

<button class="menu-toggle" aria-expanded="false" aria-controls="global-nav">
  <span class="sr-only">Open menu</span>☰
</button>
<?php wp_nav_menu([
  'theme_location' => 'global',
  'container'      => false,
  'items_wrap'     => '<ul id="global-nav" class="global-nav">%3$s</ul>',
]); ?>
const toggle = document.querySelector('.menu-toggle');
const nav = document.getElementById('global-nav');

toggle?.addEventListener('click', () => {
  const open = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', String(!open));
  nav.classList.toggle('is-open');
});

Toggling aria-expanded communicates the open/closed state to assistive technology as well.

TipsThe right way to load CSS and JavaScript (wp_enqueue)How to load this JS file from your theme

Building the menu HTML entirely yourself

When matching wp_nav_menu()’s output is more trouble than it’s worth, you can fetch just the item data and build your own.

<?php
$locations = get_nav_menu_locations();
$items = !empty($locations['global']) ? wp_get_nav_menu_items($locations['global']) : [];
?>
<?php if ($items) : ?>
  <ul class="global-nav">
    <?php foreach ($items as $item) : ?>
      <?php if ((int) $item->menu_item_parent === 0) : ?>
        <li>
          <a href="<?php echo esc_url($item->url); ?>"><?php echo esc_html($item->title); ?></a>
        </li>
      <?php endif; ?>
    <?php endforeach; ?>
  </ul>
<?php endif; ?>

Items whose menu_item_parent is 0 are the top level. You get complete control of the structure, but in exchange you’re responsible for things like the current-page class.

Listing pages without using menus

On a small site where “just list the static pages automatically” is enough, this is easier.

<?php wp_list_pages(['title_li' => '', 'depth' => 1]); ?>

But the order and which pages appear can’t be chosen from the admin screen, so on a site you’re delivering, the menu feature is the kinder option.

Theme developmentA navigation menu you can edit from the admin screenWant to learn this as part of building a theme? Head to this lesson of the theme development course

Summary

  1. Register locations with register_nav_menus() and call them with wp_nav_menu()
  2. Nothing appears until you tick “Display location” in the admin screen
  3. Remove unwanted markup with container: false and items_wrap
  4. Use the automatic classes like current-menu-item in CSS to build highlighting and dropdowns

Leaving menus editable from the admin screen means “please add one link” stops arriving as a job for you.

FAQ

My menu doesn't appear.
After writing register_nav_menus() in functions.php, you also need to create a menu under "Appearance" → "Menus" and tick it under "Display location." Forgetting to assign the location is by far the most common cause.
I want to remove the extra div and ul that wp_nav_menu outputs.
'container' => false removes the div, and 'items_wrap' => '<ul class="my-nav">%3$s</ul>' lets you specify the ul markup itself. If you only want the li elements, set items_wrap to '%3$s'.
I want to highlight the menu item for the current page.
WordPress automatically adds a current-menu-item class (current-menu-ancestor on parents), so style that class in CSS.
I want to build the menu HTML entirely myself.
Get an array of items with wp_get_nav_menu_items() and build your own HTML with foreach. For finer control, you can also extend Walker_Nav_Menu.