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 のキーをそろえる

関連記事

お気軽にご相談ください

お見積りへ お問い合わせへ
▲