2026年6月17日
2026年6月17日
WordPressナビゲーションメニューのカスタマイズ方法(フィルター・Walker・カスタムフィールド)
WordPress のナビゲーションメニューは、register_nav_menus() で位置を登録し、wp_nav_menu() で出力します。出力される HTML は、フィルターでクラスや属性を足すか、Walker_Nav_Menu を継承したクラス(Walker)で組み立て方を変えることでカスタマイズできます。
この記事は、functions.php とテンプレートファイルを使うクラシックテーマが対象です。コードは WordPress 6.9 と PHP 8.3 で動作を確認しています(2026年10月時点)。ブロックテーマのナビゲーションブロックは仕組みが別のため、末尾で簡単に触れます。
functions.php を編集する前にバックアップを取り、テーマを更新しても消えないよう子テーマに書くことをおすすめします。
メニューの位置を登録する
テーマの functions.php で、after_setup_theme フックから register_nav_menus() を呼びます。キー(primary など)がテンプレートから指定する名前、値が管理画面に表示される説明です。
<?php
add_action( 'after_setup_theme', function () {
register_nav_menus(
array(
'primary' => 'グローバルナビゲーション',
'footer' => 'フッターメニュー',
)
);
} );
登録すると、テーマがメニュー機能に対応した扱いになり、管理画面に「外観」→「メニュー」が表示されます。メニューを作ったら、画面下部の「メニュー設定」で表示する位置にチェックを入れて「メニューを保存」を押すか、「位置を管理」タブで位置ごとにメニューを割り当てます。
wp_nav_menu() でメニューを表示する
ヘッダーのテンプレート(header.php など)で、登録した位置のメニューを出力します。
<?php
if ( has_nav_menu( 'primary' ) ) {
wp_nav_menu(
array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'main-navigation',
'container_aria_label' => 'メインメニュー',
'menu_class' => 'navbar-nav',
'depth' => 2,
'fallback_cb' => false,
)
);
}
主な引数は次のとおりです。
| 引数 | 意味 |
|---|---|
theme_location |
register_nav_menus() で登録したキー |
container |
外側を囲むタグ。nav か div、囲まない場合は false |
container_aria_label |
container が nav のときに付ける aria-label |
menu_class |
ul 要素のクラス |
depth |
表示する階層の深さ。0 は制限なし |
fallback_cb |
メニューが見つからないときに呼ぶ関数。既定は固定ページの一覧を出す wp_page_menu。false で何も出さない |
items_wrap |
ul の書式。既定は <ul id="%1$s" class="%2$s">%3$s</ul>(%3$s が項目) |
walker |
HTML を組み立てる Walker のインスタンス |
has_nav_menu() は、その位置にメニューが割り当てられているかを返します。割り当てがないときに何も出さない場合は、このように囲みます。
フィルターでクラスや属性を足す(Bootstrap 5 の例)
クラスや属性を足すだけなら、Walker を書かなくてもフィルターで足ります。Walker_Nav_Menu は出力の途中で次のフィルターを通すため、標準の構造(ul と li と a)を保ったまま変更できます。
| フィルター | 対象 | 受け取る引数 |
|---|---|---|
nav_menu_css_class |
li のクラス |
$classes, $menu_item, $args, $depth |
nav_menu_link_attributes |
a の属性 |
$atts, $menu_item, $args, $depth |
nav_menu_submenu_css_class |
子メニューの ul のクラス |
$classes, $args, $depth |
nav_menu_item_title |
リンクの文字 | $title, $menu_item, $args, $depth |
次の例は、primary の位置のメニューに Bootstrap 5 のナビゲーションとドロップダウンのクラスを付けます。$args->theme_location で位置を確かめ、他のメニューには影響させないようにしています。
<?php
// li に nav-item、子を持つ項目には dropdown を付ける
add_filter( 'nav_menu_css_class', function ( $classes, $menu_item, $args, $depth ) {
if ( 'primary' !== $args->theme_location ) {
return $classes;
}
if ( 0 === $depth ) {
$classes[] = 'nav-item';
if ( in_array( 'menu-item-has-children', $classes, true ) ) {
$classes[] = 'dropdown';
}
}
return $classes;
}, 10, 4 );
// a に nav-link / dropdown-toggle / dropdown-item と、ドロップダウン用の属性を付ける
add_filter( 'nav_menu_link_attributes', function ( $atts, $menu_item, $args, $depth ) {
if ( 'primary' !== $args->theme_location ) {
return $atts;
}
if ( $depth > 0 ) {
$atts['class'] = 'dropdown-item';
return $atts;
}
$atts['class'] = 'nav-link';
if ( in_array( 'menu-item-has-children', (array) $menu_item->classes, true ) ) {
$atts['class'] .= ' dropdown-toggle';
$atts['role'] = 'button';
$atts['data-bs-toggle'] = 'dropdown';
$atts['aria-expanded'] = 'false';
}
return $atts;
}, 10, 4 );
// 子メニューの ul に dropdown-menu を付ける
add_filter( 'nav_menu_submenu_css_class', function ( $classes, $args, $depth ) {
if ( 'primary' === $args->theme_location ) {
$classes[] = 'dropdown-menu';
}
return $classes;
}, 10, 3 );
出力は次のようになります(一部省略)。
<ul id="menu-main" class="navbar-nav">
<li class="menu-item ... nav-item"><a href="/" class="nav-link">Home</a></li>
<li class="menu-item menu-item-has-children ... nav-item dropdown">
<a href="/services/" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">Services</a>
<ul class="sub-menu dropdown-menu">
<li class="menu-item ..."><a href="/services/web/" class="dropdown-item">Web</a></li>
</ul>
</li>
</ul>
注意点が2つあります。
- ドロップダウンが開くには、テーマで Bootstrap の CSS と JavaScript(
bootstrap.bundle.min.jsなど)を読み込んでおく必要があります。 - Bootstrap のドロップダウンでは、親項目をクリックするとメニューが開くだけで、親項目のリンク先には移動しません。親のページにも移動させたい場合は、そのページを子メニューの先頭にも入れておきます。
Walker で HTML の構造を変える
li を使わないなど、HTML の構造そのものを変えたいときは Walker_Nav_Menu を継承します。上書きできる主なメソッドは次の4つです。
| メソッド | 呼ばれるとき |
|---|---|
start_lvl( &$output, $depth = 0, $args = null ) |
子メニューの始まり(既定は <ul class="sub-menu">) |
end_lvl( &$output, $depth = 0, $args = null ) |
子メニューの終わり |
start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) |
各項目の始まり(既定は <li> と <a>) |
end_el( &$output, $data_object, $depth = 0, $args = null ) |
各項目の終わり(既定は </li>) |
上書きするときは、引数の並びと既定値を親クラスと同じにし、型宣言を付けません。親クラスにない型宣言(string &$output や WP_Post $item など)を付けると、PHP の互換性チェックで「must be compatible with」の致命的エラーになります。
次の例は、フッター用に li を使わず a だけを並べる Walker です。
<?php
class My_Flat_Link_Walker extends Walker_Nav_Menu {
public function start_lvl( &$output, $depth = 0, $args = null ) {}
public function end_lvl( &$output, $depth = 0, $args = null ) {}
public function start_el( &$output, $data_object, $depth = 0, $args = null, $current_object_id = 0 ) {
$menu_item = $data_object;
$is_current = in_array( 'current-menu-item', (array) $menu_item->classes, true );
$output .= sprintf(
'<a href="%1$s"%2$s>%3$s</a>',
esc_url( $menu_item->url ),
$is_current ? ' class="is-current" aria-current="page"' : '',
esc_html( $menu_item->title )
);
}
public function end_el( &$output, $data_object, $depth = 0, $args = null ) {}
}
テンプレートでは items_wrap を %3$s にして ul を出さないようにし、walker に渡します。
<?php
wp_nav_menu(
array(
'theme_location' => 'footer',
'container' => 'nav',
'items_wrap' => '%3$s',
'depth' => 1,
'walker' => new My_Flat_Link_Walker(),
)
);
start_el() を自分で書くと、標準の li と a に付くクラスや target・rel などの属性、前述のフィルターは使われなくなります。必要なものは自分で出力してください。クラスや属性を足すだけの場合は、Walker より前の節のフィルターの方が保守しやすくなります。
ログイン状態でメニュー項目を足す
wp_nav_menu_items フィルターは、組み立てた項目の HTML(li の並び)を受け取ります。末尾に文字列を足すと、ログイン状態に応じた項目を出せます。
<?php
add_filter( 'wp_nav_menu_items', function ( $items, $args ) {
if ( 'primary' !== $args->theme_location ) {
return $items;
}
if ( is_user_logged_in() ) {
$items .= '<li class="menu-item nav-item"><a class="nav-link" href="' . esc_url( admin_url() ) . '">管理画面</a></li>';
$items .= '<li class="menu-item nav-item"><a class="nav-link" href="' . esc_url( wp_logout_url( home_url( '/' ) ) ) . '">ログアウト</a></li>';
} else {
$redirect = is_singular() ? get_permalink() : home_url( '/' );
$items .= '<li class="menu-item nav-item"><a class="nav-link" href="' . esc_url( wp_login_url( $redirect ) ) . '">ログイン</a></li>';
}
return $items;
}, 10, 2 );
wp_login_url() の引数はログイン後に戻る先です。get_permalink() はアーカイブページなどでは使えないため、個別ページのときだけ渡しています。ページキャッシュのプラグインを使っている場合、ログインしていない訪問者向けのページがキャッシュされるため、ログイン中の表示が反映されないことがあります。
メニュー項目にアイコンの入力欄を足す
WordPress 5.4 以降は、「外観」→「メニュー」の各項目の設定欄に wp_nav_menu_item_custom_fields アクションで入力欄を足せます。保存は wp_update_nav_menu_item アクションで行います。
ここで間違えやすいのは、保存処理で読む $_POST のキーを入力欄の name と同じにすることです。入力欄を name="menu-item-icon[ID]" にしたなら、保存処理でも $_POST['menu-item-icon'] を読みます(menu_item_icon のように綴りが違うと保存されません)。
<?php
// 1. 入力欄を表示する
add_action( 'wp_nav_menu_item_custom_fields', function ( $item_id, $menu_item ) {
$icon = get_post_meta( $item_id, '_menu_item_icon', true );
?>
<p class="field-icon description description-wide">
<label for="edit-menu-item-icon-<?php echo esc_attr( $item_id ); ?>">
アイコンのクラス名(例: fa-solid fa-house)<br>
<input type="text" class="widefat"
id="edit-menu-item-icon-<?php echo esc_attr( $item_id ); ?>"
name="menu-item-icon[<?php echo esc_attr( $item_id ); ?>]"
value="<?php echo esc_attr( $icon ); ?>">
</label>
</p>
<?php
}, 10, 2 );
// 2. 保存する
add_action( 'wp_update_nav_menu_item', function ( $menu_id, $menu_item_db_id ) {
// 「外観」→「メニュー」の保存のときだけ処理する
if ( ! isset( $_POST['update-nav-menu-nonce'] )
|| ! wp_verify_nonce( wp_unslash( $_POST['update-nav-menu-nonce'] ), 'update-nav_menu' )
|| ! current_user_can( 'edit_theme_options' ) ) {
return;
}
// 入力欄の name="menu-item-icon[ID]" と同じキーで読む
if ( ! isset( $_POST['menu-item-icon'][ $menu_item_db_id ] ) ) {
return;
}
$raw = wp_unslash( $_POST['menu-item-icon'][ $menu_item_db_id ] );
$classes = array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', $raw ) ) );
if ( $classes ) {
update_post_meta( $menu_item_db_id, '_menu_item_icon', implode( ' ', $classes ) );
} else {
delete_post_meta( $menu_item_db_id, '_menu_item_icon' );
}
}, 10, 2 );
// 3. リンクの文字の前にアイコンを出す
add_filter( 'nav_menu_item_title', function ( $title, $menu_item, $args, $depth ) {
$icon = get_post_meta( $menu_item->ID, '_menu_item_icon', true );
if ( $icon ) {
$title = '<i class="' . esc_attr( $icon ) . '" aria-hidden="true"></i> ' . $title;
}
return $title;
}, 10, 4 );
コードのポイントは次のとおりです。
- 「外観」→「メニュー」のフォームには、WordPress 本体が
update-nav-menu-nonceという名前のノンスを入れて送り、保存前に検証しています。保存処理ではこのノンスを確かめ、メニュー画面からの保存のときだけメタデータを書き換えます。項目ごとに自前のノンス欄を出す必要はありません。 - 入力値は空白で分けてから
sanitize_html_class()でクラス名として安全な文字だけにし、空なら削除します。 - アイコンを表示するには、Font Awesome などのアイコンフォントをテーマで読み込んでおく必要があります。
- この入力欄が出るのは「外観」→「メニュー」の画面です。カスタマイザーのメニュー編集には表示されません。
ブロックテーマの場合
Twenty Twenty-Five などのブロックテーマでは、サイトエディターのナビゲーションブロックでメニューを作ります。メニューは wp_navigation という投稿として保存され、この記事の wp_nav_menu() やフィルター、Walker は使われません。
ブロックテーマが register_nav_menus() を呼ばない場合は「外観」→「メニュー」も表示されません。クラシックテーマから移行するときは、ナビゲーションブロックのメニュー選択から、既存のクラシックメニューを取り込めます。
まとめ
- メニューの位置は
after_setup_themeでregister_nav_menus()を呼んで登録し、テンプレートでwp_nav_menu()にtheme_locationを指定して出力する - クラスや属性を足すだけなら、
nav_menu_css_class・nav_menu_link_attributes・nav_menu_submenu_css_classのフィルターで足りる - HTML の構造を変えるときは
Walker_Nav_Menuを継承し、メソッドの引数は親クラスと同じにして型宣言を付けない - ログイン状態で項目を変えるには
wp_nav_menu_itemsフィルターを使う - メニュー項目のカスタムフィールドは、入力欄の
nameと保存処理で読む$_POSTのキーをそろえる