Se stai sviluppando un tema WordPress da zero (o stai mettendo mano a un tema classico ereditato da un altro sviluppatore), prima o poi ti imbatti nella funzione register_nav_menus. È il punto di partenza per dare al cliente un pannello Aspetto > Menu funzionante e per stampare la navigazione nel template senza hardcodare link nell’header.
In questa guida di Mumble Studio vediamo il flusso completo: dichiarazione delle posizioni in functions.php, output con wp_nav_menu(), gestione delle classi CSS generate automaticamente da WordPress, un menu responsive accessibile e infine un Walker personalizzato per controllare l’HTML riga per riga. Tutti gli snippet sono pronti da copiare e adattare.
Cosa fa esattamente register_nav_menus
register_nav_menus() non crea un menu: registra una o più posizioni (theme locations) a cui l’amministratore potrà poi assegnare un menu creato dal backend. In altre parole, il tema dichiara i “contenitori”, il contenuto lo decide chi gestisce il sito. sejuku.net entra nei numeri.
La funzione, inoltre, aggiunge automaticamente il supporto add_theme_support( 'menus' ): non serve dichiararlo separatamente.
| Funzione | Quando usarla | Parametro |
|---|---|---|
register_nav_menus() |
Registra più posizioni in una sola chiamata (il caso più comune) | Array associativo slug => etichetta |
register_nav_menu() |
Registra una singola posizione | $location, $description |
unregister_nav_menu() |
Rimuove una posizione di un tema padre da un child theme | $location |
has_nav_menu() |
Verifica se a una posizione è stato assegnato un menu | $location |

Passo 1: dichiarare i menu in functions.php
La registrazione va sempre agganciata all’hook after_setup_theme. Farlo fuori da un hook funziona quasi sempre, ma è una pessima pratica: in un child theme rischi problemi di ordine di caricamento e di traduzione delle stringhe.
<?php
/**
* Registra le posizioni dei menu del tema.
*/
function mumble_register_menus() {
register_nav_menus(
array(
'primary' => esc_html__( 'Menu principale', 'mumble' ),
'footer' => esc_html__( 'Menu footer', 'mumble' ),
'legal' => esc_html__( 'Menu legale (privacy, cookie)', 'mumble' ),
'social' => esc_html__( 'Menu social', 'mumble' ),
)
);
}
add_action( 'after_setup_theme', 'mumble_register_menus' );
Regole pratiche sugli slug:
- usa solo lettere minuscole, numeri e trattini:
primary,footer-1,menu-servizi; - non cambiarli dopo la messa online: se rinomini uno slug, l’associazione salvata nel database si perde e il menu “sparisce” dal front end;
- l’etichetta (il secondo valore) è quella che vede il cliente in Aspetto > Menu: scrivila in modo comprensibile, non tecnico.
Registrare una sola posizione
add_action( 'after_setup_theme', function() {
register_nav_menu( 'primary', esc_html__( 'Menu principale', 'mumble' ) );
} );
Rimuovere una posizione dal tema padre
add_action( 'after_setup_theme', 'mumble_child_unregister', 11 );
function mumble_child_unregister() {
unregister_nav_menu( 'social' );
}
Nota la priorità 11: deve girare dopo la registrazione del tema padre.
Passo 2: assegnare il menu dal backend
Dopo aver salvato functions.php, il cliente (o tu) deve completare l’operazione lato amministrazione:
- Vai su Aspetto > Menu.
- Clicca su Crea un nuovo menu, assegna un nome (es. “Navigazione sito”) e salva.
- Aggiungi pagine, articoli, categorie o link personalizzati.
- Nel box Impostazioni menu spunta la posizione registrata (es. “Menu principale”).
- Salva il menu.
Se il box delle posizioni è vuoto, la registrazione non è andata a buon fine: controlla di aver modificato il functions.php del tema attivo. C’è altro in The WordPress Navigation Menu: How It Works.

Passo 3: stampare il menu nel template con wp_nav_menu
Nel file header.php (o in un template part) richiami la posizione tramite l’argomento theme_location:
<?php if ( has_nav_menu( 'primary' ) ) : ?>
<nav class="main-nav" aria-label="<?php esc_attr_e( 'Navigazione principale', 'mumble' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => false,
'menu_id' => 'primary-menu',
'menu_class' => 'nav-list',
'depth' => 2,
'fallback_cb' => false,
)
);
?>
</nav>
<?php endif; ?>
Gli argomenti che servono davvero
| Argomento | Default | A cosa serve |
|---|---|---|
theme_location |
vuoto | Slug registrato con register_nav_menus |
menu |
vuoto | Forza un menu specifico per ID, slug o nome (utile per output condizionali) |
container |
div |
Tag wrapper. Metti false se il <nav> lo scrivi a mano |
menu_class / menu_id |
menu |
Classe e id applicati all’<ul> |
depth |
0 | Livelli stampati. 1 disabilita i sottomenu |
fallback_cb |
wp_page_menu |
Cosa mostrare se nessun menu è assegnato. Con false non stampa nulla |
walker |
Walker_Nav_Menu |
Classe che genera il markup (vedi sotto) |
echo |
true |
Con false restituisce la stringa invece di stamparla |
Le classi CSS generate da WordPress
Ogni <li> riceve un set di classi automatiche. Conoscerle evita di scrivere CSS a tentativi:
| Classe | Significato |
|---|---|
menu-item |
Presente su tutte le voci |
menu-item-123 |
ID univoco della voce di menu |
menu-item-type-post_type |
Tipo di oggetto collegato (post_type, taxonomy, custom) |
menu-item-object-page |
Oggetto specifico (page, post, category…) |
menu-item-has-children |
La voce ha un sottomenu: fondamentale per i dropdown |
current-menu-item |
Pagina attualmente visualizzata |
current-menu-parent / current-menu-ancestor |
Genitore o antenato della pagina corrente |
menu-item-home |
Voce che punta alla home page |
Ripulire le classi con il filtro nav_menu_css_class
Sulle strutture grandi WordPress stampa 6-7 classi per voce, con un HTML pesantissimo. Se lavori con una metodologia BEM o utility-first, puoi filtrare l’output:
add_filter( 'nav_menu_css_class', 'mumble_clean_menu_classes', 10, 3 );
function mumble_clean_menu_classes( $classes, $item, $args ) {
if ( empty( $args->theme_location ) || 'primary' !== $args->theme_location ) {
return $classes;
}
$allowed = array(
'menu-item-has-children',
'current-menu-item',
'current-menu-ancestor',
);
$classes = array_intersect( $classes, $allowed );
$classes[] = 'nav-list__item';
return $classes;
}
Filtri gemelli utili: nav_menu_item_id (rimuove gli id menu-item-123), nav_menu_link_attributes (aggiunge attributi al tag <a>) e nav_menu_submenu_css_class (classi dell’<ul> figlio).
add_filter( 'nav_menu_link_attributes', function( $atts, $item, $args, $depth ) {
$atts['class'] = 'nav-link nav-link--level-' . $depth;
if ( in_array( 'current-menu-item', (array) $item->classes, true ) ) {
$atts['aria-current'] = 'page';
}
return $atts;
}, 10, 4 );
Menu responsive: markup, CSS e toggle accessibile
Il pattern che usiamo più spesso nei progetti: un bottone hamburger con aria-expanded, il menu che diventa off-canvas sotto i 992px e i sottomenu che si aprono a click su mobile e a hover su desktop.
1. Il markup nell’header
<header class="site-header">
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
<span class="menu-toggle__bar"></span>
<span class="screen-reader-text"><?php esc_html_e( 'Apri il menu', 'mumble' ); ?></span>
</button>
<nav id="site-navigation" class="main-nav" aria-label="<?php esc_attr_e( 'Principale', 'mumble' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => false,
'menu_id' => 'primary-menu',
'menu_class' => 'nav-list',
'depth' => 2,
'fallback_cb' => false,
)
);
?>
</nav>
</header>
2. Il CSS essenziale
.menu-toggle { display: block; }
.nav-list {
display: none;
list-style: none;
margin: 0;
padding: 0;
}
.main-nav.is-open .nav-list { display: block; }
.nav-list .sub-menu {
display: none;
list-style: none;
padding-left: 1rem;
}
.nav-list .menu-item-has-children.is-open > .sub-menu { display: block; }
@media (min-width: 992px) {
.menu-toggle { display: none; }
.nav-list {
display: flex;
gap: 1.5rem;
}
.nav-list .sub-menu {
position: absolute;
min-width: 220px;
padding: .5rem 0;
background: #fff;
box-shadow: 0 8px 24px rgba(0,0,0,.12);
}
.nav-list .menu-item-has-children:hover > .sub-menu,
.nav-list .menu-item-has-children:focus-within > .sub-menu { display: block; }
}
3. Il JavaScript (vanilla, senza dipendenze)
document.addEventListener( 'DOMContentLoaded', function () {
var toggle = document.querySelector( '.menu-toggle' );
var nav = document.querySelector( '.main-nav' );
if ( toggle && nav ) {
toggle.addEventListener( 'click', function () {
var open = toggle.getAttribute( 'aria-expanded' ) === 'true';
toggle.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
nav.classList.toggle( 'is-open' );
} );
}
document.querySelectorAll( '.submenu-toggle' ).forEach( function ( btn ) {
btn.addEventListener( 'click', function () {
var parent = btn.closest( '.menu-item-has-children' );
var open = btn.getAttribute( 'aria-expanded' ) === 'true';
btn.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
parent.classList.toggle( 'is-open' );
} );
} );
} );
Ricorda di accodare lo script correttamente:
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_script(
'mumble-navigation',
get_theme_file_uri( '/assets/js/navigation.js' ),
array(),
wp_get_theme()->get( 'Version' ),
true
);
} );

Walker personalizzato: controllo totale sull’HTML
I filtri coprono l’80% dei casi. Quando serve una struttura diversa (bottoni di apertura sottomenu, icone, descrizioni delle voci, wrapper interni al link) si estende la classe Walker_Nav_Menu.
<?php
/**
* Walker personalizzato per il menu principale.
* File: /inc/class-mumble-walker-nav-menu.php
*/
class Mumble_Walker_Nav_Menu extends Walker_Nav_Menu {
/* Apertura del sottomenu */
public function start_lvl( &$output, $depth = 0, $args = null ) {
$output .= '<ul class="sub-menu sub-menu--level-' . ( $depth + 1 ) . '">';
}
/* Chiusura del sottomenu */
public function end_lvl( &$output, $depth = 0, $args = null ) {
$output .= '</ul>';
}
/* Apertura della singola voce */
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$classes[] = 'nav-list__item';
$has_children = in_array( 'menu-item-has-children', $classes, true );
$is_current = in_array( 'current-menu-item', $classes, true );
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
$output .= '<li' . $class_names . '>';
$atts = array(
'href' => ! empty( $item->url ) ? $item->url : '#',
'title' => ! empty( $item->attr_title ) ? $item->attr_title : '',
'target' => ! empty( $item->target ) ? $item->target : '',
'rel' => ! empty( $item->xfn ) ? $item->xfn : '',
'class' => 'nav-link nav-link--depth-' . $depth,
'aria-current' => $is_current ? 'page' : '',
);
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
$attributes = '';
foreach ( $atts as $attr => $value ) {
if ( '' !== $value && false !== $value ) {
$attributes .= ' ' . $attr . '="' . esc_attr( $value ) . '"';
}
}
$title = apply_filters( 'the_title', $item->title, $item->ID );
$item_output = '<a' . $attributes . '><span class="nav-link__label">' . $title . '</span>';
if ( ! empty( $item->description ) && 0 === $depth ) {
$item_output .= '<span class="nav-link__desc">' . esc_html( $item->description ) . '</span>';
}
$item_output .= '</a>';
/* Bottone accessibile per aprire il sottomenu su mobile */
if ( $has_children ) {
$item_output .= '<button type="button" class="submenu-toggle" aria-expanded="false">'
. '<span class="screen-reader-text">' . esc_html__( 'Apri sottomenu', 'mumble' ) . '</span>'
. '</button>';
}
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
/* Chiusura della singola voce */
public function end_el( &$output, $item, $depth = 0, $args = null ) {
$output .= '</li>';
}
}
Includi il file e passa la classe a wp_nav_menu():
require_once get_theme_file_path( '/inc/class-mumble-walker-nav-menu.php' );
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'nav-list',
'depth' => 2,
'fallback_cb' => false,
'walker' => new Mumble_Walker_Nav_Menu(),
)
);
Attenzione: le descrizioni delle voci sono visibili nel backend solo attivando la casella “Descrizione” dal pannello Opzioni schermata, in alto a destra nella schermata dei menu.
Bonus: recuperare gli elementi del menu senza wp_nav_menu
Se devi costruire una navigazione dentro un componente custom, un blocco o una risposta REST, ti serve l’array grezzo delle voci:
$locations = get_nav_menu_locations();
if ( isset( $locations['primary'] ) ) {
$menu_obj = wp_get_nav_menu_object( $locations['primary'] );
$items = wp_get_nav_menu_items( $menu_obj->term_id );
foreach ( $items as $item ) {
// $item->title, $item->url, $item->menu_item_parent, $item->object_id
}
}
// Nome leggibile del menu assegnato a una posizione
$nome = wp_get_nav_menu_name( 'primary' );
Per recuperare un menu tramite il suo nome invece che tramite posizione basta passare la stringa: wp_get_nav_menu_items( 'Navigazione sito' ).

Problemi frequenti e come risolverli
| Sintomo | Causa tipica | Soluzione |
|---|---|---|
| La posizione non appare in Aspetto > Menu | Codice nel tema sbagliato o hook mancante | Verifica il tema attivo e usa after_setup_theme |
| Vengono stampate tutte le pagine del sito | Nessun menu assegnato, entra il fallback wp_page_menu |
Imposta 'fallback_cb' => false o assegna il menu |
| Il menu è sparito dopo un aggiornamento del tema | Slug della location rinominato | Ripristina lo slug originale o riassegna il menu |
| Errore fatale sul Walker | Firma dei metodi non compatibile con il core | Mantieni parametri e riferimenti &$output identici alla classe padre |
| I sottomenu non si aprono su desktop | depth impostato a 1 |
Porta depth a 2 o 0 |
E nei temi a blocchi (Full Site Editing)?
Nei temi a blocchi la navigazione è gestita dal blocco Navigazione e i menu classici non vengono usati per l’header. Tuttavia register_nav_menus resta indispensabile in tre scenari molto concreti:
- temi classici su misura, ancora la scelta più solida per progetti con logiche PHP complesse;
- temi ibridi, dove header e footer restano in PHP mentre i contenuti usano i blocchi;
- plugin e blocchi custom che devono leggere una struttura di menu gestibile dal cliente senza toccare l’editor.
Se stai migrando un tema classico verso i blocchi, tieni presente che il blocco Navigazione può importare un menu esistente, quindi le voci create in Aspetto > Menu non vanno perse. Vedi https://wpmudev.com.
Checklist finale
- Registra le posizioni con
register_nav_menus()dentroafter_setup_theme. - Usa slug stabili e etichette comprensibili per il cliente.
- Proteggi l’output con
has_nav_menu()e disattiva il fallback. - Sfrutta
menu-item-has-childrenecurrent-menu-itemper il CSS invece di inventare classi. - Prima prova con i filtri, poi (solo se serve) scrivi un Walker.
- Aggiungi sempre
aria-expanded,aria-labelearia-current: un menu non accessibile è un menu incompleto.
FAQ
Qual è la differenza tra register_nav_menu e register_nav_menus?
La prima registra una sola posizione e accetta due parametri (slug e descrizione), la seconda ne registra molte in una volta tramite un array associativo. A livello di funzionamento sono identiche: register_nav_menus() internamente cicla sull’array.
Devo aggiungere anche add_theme_support( ‘menus’ )?
No. Sia register_nav_menu() sia register_nav_menus() attivano automaticamente il supporto ai menu per il tema.
Come faccio a mostrare un menu diverso in base alla pagina?
Puoi registrare più posizioni e scegliere quale stampare con una condizione PHP, oppure usare l’argomento menu di wp_nav_menu() passando ID, slug o nome del menu desiderato.
Perché WordPress aggiunge tante classi ai miei li?
Servono a identificare tipo di contenuto e stato della voce (corrente, genitore, con figli). Se ti pesano, filtrale con nav_menu_css_class mantenendo almeno menu-item-has-children e current-menu-item.
Posso creare un menu via codice, senza passare dal backend?
Sì, con wp_create_nav_menu(), wp_update_nav_menu_item() e set_theme_mod( 'nav_menu_locations', ... ). È una soluzione tipica degli script di setup iniziale, ma va eseguita una sola volta con un controllo su wp_get_nav_menu_object() per evitare duplicati a ogni caricamento.
Il Walker rallenta il sito?
No, l’impatto è trascurabile: la generazione del menu avviene in memoria dopo una query molto leggera. Se hai centinaia di voci, valuta piuttosto una cache dell’output con 'echo' => false e un transient.
Hai bisogno di un tema WordPress su misura, veloce e mantenibile? In Mumble Studio sviluppiamo temi custom con architetture pulite, navigazioni accessibili e codice documentato. Parliamo del tuo progetto.