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という引数名を使っている場合は影響ありませんが、型宣言を付ける場合は注意してください。
    • Walkerクラス内でWordPressコアのフィルター(nav_menu_link_attributeswalker_nav_menu_start_elなど)をapply_filters()で呼び出すことで、他のプラグインやテーマとの互換性を保てます。
    • Walker_Nav_Menuを完全に置き換えるとnav_menu_css_classなどのフィルターが効かなくなる場合があります。コアのWalkerを参考にフィルターを引き続き呼び出してください。

    まとめ

    WalkerによるメニューカスタマイズはSQL「Walker_Nav_Menuを継承してstart_el()/start_lvl()をオーバーライド→

  • に独自クラス・data属性を追加→子メニューのトグルボタンを挿入→Bootstrap 5対応はトップレベルにdropdown-toggle・子にdropdown-itemwp_nav_menu()walker引数に渡す」の流れで整備します。関連記事:WordPressのナビゲーションメニューをカスタマイズする方法WordPressのブロックエディターにカスタムブロックを追加する方法

お気軽にご相談ください

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