KIếN THứC WEBSITE › WORDPRESS

Walker_Nav_Menu tùy biến — build mega menu WordPress

Walker_Nav_Menu tùy biến — build mega menu WordPress 2026

Hàm wp_nav_menu() mặc định trả về <ul><li><a> generic — không support icon SVG, badge count, mega menu nhiều cột, hay schema markup. Walker_Nav_Menu cho phép control 100% markup output.

Bài này hướng dẫn build custom walker với 4 method bắt buộc, inject Carbon Fields meta cho mega menu pixel-perfect, debug recursive call, và cache nav HTML qua transient.

Walker_Nav_Menu trong WordPress core và khi nào cần custom

walker_nav_menu tùy biến — Walker_Nav_Menu trong WordPress core và khi nào cần custom
Walker_Nav_Menu trong WordPress core và khi nào cần custom

Walker_Nav_Menu là class WordPress core nằm trong wp-includes/class-walker-nav-menu.php. Class này chịu trách nhiệm render menu items thành HTML string thông qua 4 method recursive: start_lvl, end_lvl, start_el, end_el.

Mặc định output mỗi item là <li class="menu-item"><a href>Title</a></li>. Đủ cho menu đơn giản 1-2 cấp, không đủ cho mega menu, icon, badge, hay accessibility nâng cao.

Năm use case bắt buộc custom Walker

Không phải nav menu nào cũng cần custom walker. Năm use case dưới là ngưỡng kinh nghiệm — nếu nav menu của em có 1 trong 5 yêu cầu này, đã đến lúc extend Walker class.

  • Icon SVG hoặc dashicon trước title: nav menu top header với icon nhỏ kế tên — markup mặc định không hỗ trợ inject element bên trong <a>.
  • Badge count realtime: giỏ hàng (cart count), notification (số tin chưa đọc), badge “Mới” — cần inject <span class="badge"> với data dynamic.
  • Mega menu nhiều cột: dropdown full-width chứa heading, image, promo card — cần wrap submenu trong grid container thay vì <ul> đơn.
  • Schema markup BreadcrumbList: add itemtype và itemscope cho structured data — Google hiểu nav như site navigation chính thức.
  • ARIA accessibility nâng cao: aria-current, aria-haspopup, aria-expanded — pass WCAG AA cho audit accessibility production.

Khi KHÔNG cần custom walker — dùng filter

Menu đơn giản chỉ cần thêm class CSS hoặc attribute nhỏ thì không cần extend class. WordPress cung cấp 3 filter mạnh giúp tùy biến nhẹ mà không phải viết walker 80-100 dòng.

  • nav_menu_css_class: add hoặc remove class trong <li> dựa trên meta hoặc condition — phù hợp highlight current section.
  • walker_nav_menu_start_el: filter HTML output mỗi item trước khi append vào output buffer — đủ cho add icon đơn giản.
  • Tiết kiệm 80-100 dòng code: filter ngắn 5-10 dòng vs walker full 80-100 dòng — chọn filter trước, escalate sang walker khi filter không đủ.

Bốn method bắt buộc trong Walker class

Bốn method bắt buộc trong Walker class
Sơ đồ minh hoạ — Bốn method bắt buộc trong Walker class
Bốn method bắt buộc trong Walker class
Sơ đồ minh hoạ — Bốn method bắt buộc trong Walker class

Custom walker extend từ Walker_Nav_Menu và override tối thiểu 4 method. Mỗi method nhận tham số &$output by reference — em append HTML string vào biến này thay vì echo trực tiếp.

WordPress sẽ gọi 4 method này theo thứ tự recursive khi render menu tree. Hiểu lifecycle gọi giúp em đặt logic đúng chỗ, không bị thiếu wrapper hoặc miss attribute.

start_lvl() — open submenu wrapper

Method chạy mỗi khi gặp item có children. Output <ul> open tag để wrap submenu.

Đây là nơi inject class wrapper cho mega menu hoặc submenu thường.

function start_lvl(&$output, $depth = 0, $args = null) {
    $indent = str_repeat("t", $depth);
    $class  = $depth === 0 ? 'mega' : 'sub-menu';
    $output .= "n{$indent}<ul class="{$class}" data-depth="{$depth}">n";
}

end_lvl() — close submenu wrapper

Method mirror của start_lvl. Output </ul> close tag để đóng submenu.

Phải match chính xác với start_lvl — nếu start mở 2 tag, end phải đóng 2 tag.

function end_lvl(&$output, $depth = 0, $args = null) {
    $indent = str_repeat("t", $depth);
    $output .= "{$indent}</ul>n";
}

start_el() — open menu item (method quan trọng nhất)

Method nơi em build <li><a> với class, attribute, icon, badge. Đây là trái tim của custom walker — 80% logic tuỳ biến nằm ở đây.

function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) {
    $classes   = empty($item->classes) ? [] : (array) $item->classes;
    $classes[] = 'menu-item-' . $item->ID;

    if (in_array('menu-item-has-children', $classes, true)) {
        $classes[] = 'has-mega';
    }

    $class_str = esc_attr(implode(' ', array_filter($classes)));
    $output .= "<li class="{$class_str}">";

    $icon  = get_post_meta($item->ID, '_w22_icon', true);
    $badge = get_post_meta($item->ID, '_w22_badge', true);

    $attr = ' href="' . esc_url($item->url) . '"';
    if ($item->current) $attr .= ' aria-current="page"';
    if ($item->target)  $attr .= ' target="' . esc_attr($item->target) . '"';

    $output .= "<a{$attr}>";
    if ($icon) {
        $output .= '<span class="ico">' . wp_kses($icon, [
            'svg'  => ['xmlns' => [], 'viewBox' => []],
            'path' => ['d' => []],
        ]) . '</span>';
    }
    $output .= '<span class="t">' . esc_html($item->title) . '</span>';
    if ($badge) {
        $output .= '<span class="badge">' . esc_html($badge) . '</span>';
    }
    $output .= '</a>';
}

end_el() — close menu item

Method ngắn nhất — chỉ output </li>. Không cần logic vì start_el đã handle mọi attribute.

Method này rất hiếm khi cần custom.

function end_el(&$output, $item, $depth = 0, $args = null) {
    $output .= "</li>n";
}

Custom output cho mega menu 3 cột

Mega menu khác submenu dropdown thông thường ở 3 điểm cốt lõi: layout grid nhiều cột thay vì list dọc, có heading/image/promo card chứ không chỉ link, và full-width thay vì align theo parent item.

Render mega menu yêu cầu thay markup wrapper hoàn toàn — không thể chỉ thêm class. Pattern dưới detect trigger qua menu meta hoặc class CSS, sau đó render layout khác cho cấp 0.

Detect mega trigger qua menu meta

Admin tag menu item là “mega trigger” qua Carbon Fields meta hoặc class CSS thủ công has-mega. Walker check class này để render layout grid thay vì <ul> đơn.

function start_lvl(&$output, $depth = 0, $args = null) {
    if ($depth === 0) {
        $output .= '<div class="mega">';
        $output .= '<div class="mega-grid">';
        $output .= '<ul class="col">';
    } else {
        $output .= '<ul class="sub-menu">';
    }
}

function end_lvl(&$output, $depth = 0, $args = null) {
    if ($depth === 0) {
        $output .= '</ul></div></div>';
    } else {
        $output .= '</ul>';
    }
}

Split cột tự động theo số item

Mega menu 12 item nên chia 3 cột (4 item/cột), 18 item chia 3 cột (6 item/cột). Logic split nằm trong start_el — check counter modulo và chèn close-open </ul><ul> giữa cột.

  • Đếm item ở cấp 1 (depth 1): increment counter mỗi start_el khi $depth === 1 — biết đang ở item thứ mấy của cột hiện tại.
  • Chia 3 cột với 12+ item: chèn </ul><ul class="col"> giữa cột — CSS grid 3 cột tự align.
  • Responsive collapse mobile: CSS @media max-width 768px chuyển grid 3 cột thành 1 cột stack — UX mobile không bị chật.

Inject icon và badge từ Carbon Fields meta

Carbon Fields cho phép thêm custom field vào menu item ngay trong Appearance → Menus. Pattern này tách concern: admin set icon/badge qua UI thân thiện, walker chỉ đọc meta để render — không hard-code trong PHP.

Plugin Carbon Fields free và dễ tích hợp. Nếu không dùng Carbon Fields, em có thể thay bằng get_post_meta() với custom field từ ACF hoặc Meta Box.

Setup Carbon Fields cho menu item

Đoạn code dưới register 3 field cho menu item: icon SVG markup, badge label, description hiển thị trong mega menu.

add_action('carbon_fields_register_fields', function () {
    Carbon_FieldsContainer::make('nav_menu_item', 'Web22 Menu Meta')
        ->add_fields([
            Carbon_FieldsField::make('textarea', 'w22_icon', 'Icon SVG (paste markup)'),
            Carbon_FieldsField::make('text', 'w22_badge', 'Badge label (Mới, Hot, Sale)'),
            Carbon_FieldsField::make('text', 'w22_desc', 'Description (mega menu)'),
        ]);
});

Đọc Carbon meta trong walker

Walker đọc meta qua carbon_get_nav_menu_item_meta($item->ID, 'w22_icon') hoặc fallback get_post_meta($item->ID, '_w22_icon', true) nếu không dùng Carbon Fields.

Sanitize SVG trước khi output

SVG paste từ admin có thể chứa script độc hại — XSS risk nếu output thẳng. Bọc trong wp_kses với whitelist tag SVG cụ thể để strip mọi tag không an toàn.

  • Whitelist tối thiểu: chỉ cho phép svg, path, circle, rect, line — đủ cho 95% icon thông thường.
  • Whitelist attribute: xmlns, viewBox, d, fill, stroke, width, height — đủ cho icon static.
  • Cấm script và onclick: wp_kses mặc định strip <script> và event handler — defense in depth, không trust input admin tuyệt đối.

Attach walker vào wp_nav_menu

Pass instance walker vào tham số walker của wp_nav_menu. Pattern đơn giản nhưng nhiều dev sai chỗ truyền class name string thay vì instance.

wp_nav_menu([
    'theme_location' => 'primary',
    'menu_class'     => 'nav primary',
    'container'      => false,
    'depth'          => 3,
    'walker'         => new W22_Nav_Walker(),
]);

Conditional walker theo location

Theme thường có 3 menu khác nhau: primary header, footer link, social icon. Mỗi menu có thể dùng walker khác.

Wrap trong template tag để clean code.

function w22_nav($location) {
    $walker = match ($location) {
        'primary' => new W22_Mega_Walker(),
        'footer'  => new W22_Footer_Walker(),
        default   => null,
    };
    wp_nav_menu([
        'theme_location' => $location,
        'walker'         => $walker,
        'container'      => false,
    ]);
}

// Trong template
w22_nav('primary');
w22_nav('footer');

Match statement chỉ PHP 8.0+

Cú pháp match yêu cầu PHP 8.0 trở lên. WordPress core hiện vẫn support PHP 7.4 — nếu theme deploy lên hosting cũ, fallback sang switch statement.

Debug walker — dump tree và inspect recursive call

Walker khó debug vì WordPress gọi recursive — em không nhìn được flow trực quan như procedural code. Ba kỹ thuật dưới đủ cover 95% case debug walker trong thực tế.

Dump menu items array trước khi walker chạy

Filter wp_nav_menu_objects chạy trước walker, nhận array items đã prepare. Dump array này cho thấy mọi meta, class, parent ID — biết walker sẽ nhận gì.

add_filter('wp_nav_menu_objects', function ($items, $args) {
    if ($args->theme_location === 'primary' && WP_DEBUG) {
        echo '<pre style="background:#fff;padding:1em">';
        var_dump($items);
        echo '</pre>';
    }
    return $items;
}, 10, 2);

Log trace trong start_el

Tạm thêm error_log trong walker để log thứ tự call và depth. Sau đó tail wp-content/debug.log khi load page để thấy flow recursive.

function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) {
    if (WP_DEBUG) {
        error_log("start_el: {$item->title} depth={$depth} parent={$item->menu_item_parent}");
    }
    // ... rest of logic
}

Wrap output check render kết quả

Capture output qua wp_nav_menu(['echo' => false]), inspect string trước khi echo. Pattern này hữu ích khi cần test walker mà không muốn render trực tiếp lên page.

Performance — cache nav HTML qua transient

Walker chạy mỗi page load. Walker phức tạp (mega menu + Carbon meta + Schema) tốn 20-80ms render — significant với theme nhắm Lighthouse 95+.

Cache rendered HTML qua transient giảm xuống 0-2ms (chỉ read DB cache hit). Pattern an toàn dưới đây kèm invalidate khi admin update menu.

Pattern cache nav HTML

Function helper w22_cached_nav wrap wp_nav_menu với check transient trước, fallback render và cache nếu miss. Pattern này dùng output buffering để capture HTML.

function w22_cached_nav($location) {
    $key  = "w22_nav_html_{$location}";
    $html = get_transient($key);

    if ($html === false) {
        ob_start();
        wp_nav_menu([
            'theme_location' => $location,
            'walker'         => new W22_Nav_Walker(),
            'container'      => false,
        ]);
        $html = ob_get_clean();
        set_transient($key, $html, DAY_IN_SECONDS);
    }

    echo $html;
}

// Invalidate khi admin update menu
add_action('wp_update_nav_menu', function () {
    foreach (['primary', 'footer', 'social'] as $loc) {
        delete_transient("w22_nav_html_{$loc}");
    }
});

Khi KHÔNG nên cache nav HTML

Cache HTML không phù hợp mọi case. Hai trường hợp dưới phải skip cache hoặc cache theo per-user.

  • Menu có dynamic content per request: badge cart count realtime, notification user-specific — cache HTML sẽ stale rất nhanh, UX vỡ.
  • Menu có aria-current highlight: current page highlight phụ thuộc URL — cache 1 lần dùng cho mọi URL khác sẽ sai highlight.
  • Workaround per-URL cache: cache theo key + URL slug — vd w22_nav_html_primary_homepage vs w22_nav_html_primary_blog.

Câu hỏi thường gặp

Walker có hỗ trợ Block Editor Navigation block không?

Không trực tiếp. Navigation block từ WordPress 5.9 dùng React và REST API thay vì gọi wp_nav_menu.

Walker_Nav_Menu chỉ áp dụng cho hàm classic.

Custom output Navigation block cần register block variation hoặc filter render_block_core/navigation. Pattern khác hoàn toàn so với Walker — không thể reuse code walker classic cho block.

Custom walker có break accessibility không?

Có nếu quên ARIA attribute. Bắt buộc có: aria-current="page" cho current item, aria-haspopup="true"aria-expanded cho submenu trigger, role navigation trên thẻ <nav> wrapper.

Test với screen reader thực tế — NVDA cho Windows miễn phí và đáng tin cậy. Đừng tin vào automated tool 100% — chỉ tester người mới phát hiện flow điều hướng phức tạp.

Có cần escape title menu item dù WordPress đã sanitize lúc save?

Có. Defense in depth — dù WordPress sanitize input lúc save vào DB, vẫn phải esc_html() lúc output.

Title có thể chứa HTML entity bị decode sai trong một số filter chain.

Quy tắc bất di bất dịch: escape mọi output kể cả data từ DB. Cost rất thấp (vài microsecond), benefit là phòng XSS triệt để khi codebase scale lớn.

Walker class nên đặt ở đâu trong theme?

File riêng inc/class-w22-nav-walker.php trong folder inc/. Require trong inc/nav.php hoặc lazy load chỉ khi has_nav_menu() trả về true.

Tránh để class trong functions.php trực tiếp. Class 80-150 dòng phình to nhanh — đặt file riêng giúp easy maintain và git diff sạch.

Multiple walker cho cùng 1 menu — nested hay separate menus?

Tốt hơn dùng separate menu — vd “Header Primary” + “Header Mega Promo” — thay vì 1 menu phức tạp với nhiều walker nested. Admin dễ edit, walker cũng đơn giản hơn rõ rệt.

Trade-off là cần thêm menu_location trong theme registration. Web22-2026 dùng 4 location: primary, footer, social, mega-promo — mỗi cái có walker riêng phù hợp.

Tài nguyên và bước tiếp theo

Custom Walker_Nav_Menu là kỹ năng baseline cho mọi theme có nav phức tạp. Sau khi nắm 4 method và pattern Carbon meta, mở rộng sang các topic kỹ thuật khác.

Cần đội Web22 build mega menu custom + Carbon Fields admin UI cho website WordPress của bạn? Đội WordPress Developer Web22 — block + plugin + REST API — bàn giao kèm ARIA accessibility chuẩn WCAG AA, responsive mobile drawer pixel-perfect.