2026年5月27日
2026年5月27日
WordPressのウォーカークラスでメニューをカスタマイズする方法
はじめに
「WordPressのメニューHTML構造をフィルターでは制御しきれない」「Bootstrapのdropdown-menuクラスに対応したメニューを出力したい」「サブメニューにトグルボタンを挿入したい」——Walker_Nav_Menuクラスを継承することでメニューのHTML出力を完全に制御できます。
症状・原因
nav_menu_css_classなどのフィルターでは対応できない構造的な変更(例:サブメニューの
- の前にボタン要素を挿入する、
にdata-*属性を追加するなど)は、Walker_Nav_Menuを継承したカスタムWalkerクラスで対応します。
解決手順
ステップ1:Walker_Nav_Menuを継承した基本クラス
// functions.php または inc/class-walker-nav-menu.php
class My_Walker_Nav_Menu extends Walker_Nav_Menu {
/**
* サブメニュー(<ul>)の開始タグ
*/
public function start_lvl( &$output, $depth = 0, $args = null ): void {
$indent = str_repeat( "\t", $depth );
$class = 'depth-' . $depth === 0 ? 'nav__dropdown' : 'nav__sub-dropdown';
$output .= "\n{$indent}<ul class=\"{$class}\" role=\"menu\">\n";
}
/**
* サブメニュー(<ul>)の終了タグ
*/
public function end_lvl( &$output, $depth = 0, $args = null ): void {
$indent = str_repeat( "\t", $depth );
$output .= "{$indent}</ul>\n";
}
/**
* 各メニューアイテム(<li>)の開始タグ
*/
public function start_el( &$output, $data_object, $depth = 0, $args = null, $id = 0 ): void {
$item = $data_object;
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
$classes = empty( $item->classes ) ? [] : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
$classes[] = 'nav__item';
if ( in_array( 'current-menu-item', $classes, true ) ) {
$classes[] = 'is-active';
}
if ( in_array( 'menu-item-has-children', $classes, true ) ) {
$classes[] = 'has-dropdown';
}
$class_names = implode( ' ', array_filter( array_unique( $classes ) ) );
// <li> の属性
$id_attr = $item->ID ? ' id="menu-item-' . esc_attr( $item->ID ) . '"' : '';
$output .= $indent . '<li' . $id_attr . ' class="' . esc_attr( $class_names ) . '">';
// リンク属性
$atts = [];
$atts['href'] = ! empty( $item->url ) ? $item->url : '';
$atts['target'] = ! empty( $item->target ) ? $item->target : '';
$atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : '';
$atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : '';
$atts['class'] = 'nav__link';
if ( in_array( 'menu-item-has-children', $classes, true ) ) {
$atts['aria-haspopup'] = 'true';
$atts['aria-expanded'] = 'false';
}
if ( in_array( 'current-menu-item', $classes, true ) ) {
$atts['aria-current'] = 'page';
}
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
$attributes = '';
foreach ( $atts as $attr => $value ) {
if ( is_scalar( $value ) && '' !== $value && false !== $value ) {
$value = 'href' === $attr ? esc_url( $value ) : esc_attr( $value );
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$title = apply_filters( 'the_title', $item->title, $item->ID );
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
$item_output = ( $args->before ?? '' );
$item_output .= '<a' . $attributes . '>';
$item_output .= ( $args->link_before ?? '' ) . $title . ( $args->link_after ?? '' );
$item_output .= '</a>';
// 子メニューを持つアイテムにトグルボタンを追加
if ( in_array( 'menu-item-has-children', $classes, true ) ) {
$item_output .= '<button class="nav__toggle" aria-expanded="false" aria-label="サブメニューを開く">';
$item_output .= '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 4l4 4 4-4"/></svg>';
$item_output .= '</button>';
}
$item_output .= ( $args->after ?? '' );
$output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
}
}
ステップ2:WalkerをBootstrap 5対応にする
// functions.php: Bootstrap 5のdropdown対応Walker
class Bootstrap5_Walker_Nav_Menu extends Walker_Nav_Menu {
public function start_lvl( &$output, $depth = 0, $args = null ): void {
$output .= '<ul class="dropdown-menu">';
}
public function end_lvl( &$output, $depth = 0, $args = null ): void {
$output .= '</ul>';
}
public function start_el( &$output, $data_object, $depth = 0, $args = null, $id = 0 ): void {
$item = $data_object;
$classes = (array) ( $item->classes ?? [] );
$has_children = in_array( 'menu-item-has-children', $classes, true );
$is_active = in_array( 'current-menu-item', $classes, true )
|| in_array( 'current-menu-ancestor', $classes, true );
// li クラス
$li_class = 'nav-item';
if ( $has_children ) $li_class .= ' dropdown';
$output .= '<li class="' . esc_attr( $li_class ) . '">';
// リンク
$url = esc_url( $item->url );
if ( 0 === $depth ) {
if ( $has_children ) {
// トップレベルのドロップダウントリガー
$output .= sprintf(
'<a class="nav-link dropdown-toggle%s" href="%s" role="button" data-bs-toggle="dropdown" aria-expanded="false">%s</a>',
$is_active ? ' active' : '',
$url,
esc_html( $item->title )
);
} else {
$output .= sprintf(
'<a class="nav-link%s" href="%s"%s>%s</a>',
$is_active ? ' active' : '',
$url,
$is_active ? ' aria-current="page"' : '',
esc_html( $item->title )
);
}
} else {
// ドロップダウンアイテム
$output .= sprintf(
'<a class="dropdown-item%s" href="%s">%s</a>',
$is_active ? ' active' : '',
$url,
esc_html( $item->title )
);
}
}
public function end_el( &$output, $data_object, $depth = 0, $args = null ): void {
$output .= '</li>';
}
}
ステップ3:カスタムWalkerをwp_nav_menuに渡す
// テンプレートファイル(header.php): カスタムWalkerを使用
wp_nav_menu( [
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'navbar',
'menu_class' => 'nav__list',
'walker' => new My_Walker_Nav_Menu(),
'depth' => 3,
'fallback_cb' => false,
] );
// Bootstrap 5用
wp_nav_menu( [
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'navbar-nav me-auto',
'walker' => new Bootstrap5_Walker_Nav_Menu(),
'depth' => 2,
] );
ステップ4:メニューアイテムにカスタムフィールドを追加する
// functions.php: メニューアイテムの編集画面にカスタムフィールドを追加
// (「メニューの詳細設定を表示」でLink Relationship・説明・リンクターゲットが表示される)
// メニューアイテム設定を拡張してバッジテキストを追加
add_action( 'wp_nav_menu_item_custom_fields', function( $item_id, $item ): void {
$badge = get_post_meta( $item_id, '_menu_item_badge', true );
?>
<p class="field-badge description description-wide">
<label for="edit-menu-item-badge-<?php echo esc_attr( $item_id ); ?>">
バッジテキスト(例: NEW・HOT)
<input
type="text"
id="edit-menu-item-badge-<?php echo esc_attr( $item_id ); ?>"
class="widefat edit-menu-item-badge"
name="menu-item-badge[<?php echo esc_attr( $item_id ); ?>]"
value="<?php echo esc_attr( $badge ); ?>"
>
</label>
</p>
<?php
}, 10, 2 );
// バッジテキストを保存
add_action( 'wp_update_nav_menu_item', function( $menu_id, $menu_item_db_id ): void {
if ( isset( $_POST['menu-item-badge'][ $menu_item_db_id ] ) ) {
update_post_meta(
$menu_item_db_id,
'_menu_item_badge',
sanitize_text_field( $_POST['menu-item-badge'][ $menu_item_db_id ] )
);
}
}, 10, 2 );
ステップ5:Walker内でバッジを出力する
// My_Walker_Nav_Menu の start_el をオーバーライドしてバッジを追加
// (ステップ1の start_el 内、$item_output .= '</a>'; の直前に追加)
$badge = get_post_meta( $item->ID, '_menu_item_badge', true );
if ( $badge ) {
$item_output .= sprintf(
'<span class="nav__badge">%s</span>',
esc_html( $badge )
);
}
注意事項
start_el()の引数$data_objectは WordPress 6.1から型がWP_Post相当になりました。古いコードで$itemという引数名を使っている場合は影響ありませんが、型宣言を付ける場合は注意してください。nav_menu_link_attributes、walker_nav_menu_start_elなど)をapply_filters()で呼び出すことで、他のプラグインやテーマとの互換性を保てます。Walker_Nav_Menuを完全に置き換えるとnav_menu_css_classなどのフィルターが効かなくなる場合があります。コアのWalkerを参考にフィルターを引き続き呼び出してください。まとめ
WalkerによるメニューカスタマイズはSQL「Walker_Nav_Menuを継承してstart_el()/start_lvl()をオーバーライド→
dropdown-toggle・子にdropdown-item→wp_nav_menu()のwalker引数に渡す」の流れで整備します。関連記事:WordPressのナビゲーションメニューをカスタマイズする方法、WordPressのブロックエディターにカスタムブロックを追加する方法。