2026年11月12日
2026年11月12日
WordPress Mega Menu Without a Plugin: CSS Classes and CSS Grid Columns
This article is a localized edition of the Japanese original. Read the Japanese original
A mega menu shows the submenus of your main navigation not as a one-column dropdown but as a full-width panel laid out in multiple columns. On sites with many submenu items, it lets you group them as "column heading + links below it".
This article assumes a classic theme (one where you build menus under Appearance > Menus) and shows how to build a mega menu without a plugin. There are three things to do.
- In the admin screen, add the CSS class
has-mega-menuto the parent item you want to turn into a mega menu - With CSS, turn that item's submenu into a full-width panel and split it into columns with CSS Grid
- For phones and touch devices, add an open/close button with JavaScript
If you use a block theme (one where menus are built with the Navigation block), see "Block themes and plugins" at the end.
How to structure the menu
A mega menu maps the menu levels directly to columns.
| Level | Role | Example |
|---|---|---|
| 1st level | An item in the main navigation. Add has-mega-menu here |
Services |
| 2nd level | The heading of each column in the panel | Build, Maintenance, Marketing, Consulting |
| 3rd level | Links inside a column | New site, Redesign, ... |
The number of second-level items becomes the number of columns. The CSS in this article lays them out in 4 columns, so around 4 column headings is easiest to read.
Step 1: Add a CSS class to the parent item
- Go to Appearance > Menus in the admin screen.
- Open "Screen Options" at the top right and check "CSS Classes". If you want to show a description under column headings, also check "Description".
- Open the first-level item you want to turn into a mega menu and enter
has-mega-menuin "CSS Classes (optional)". - Drag the child items into the second level (column headings) and third level (links), then click "Save Menu".
When WordPress outputs a menu, it adds the class sub-menu to submenu ul elements and menu-item-has-children to li elements that have children. The CSS below uses these classes plus the has-mega-menu class you added in step 3.
Step 2: Check your theme’s class names
CSS selectors depend on the theme. Open your site, inspect the main navigation with the browser's developer tools, and note these two class names.
- The class on the menu's top
ul(nav-menuin this article; some themes usemenuorprimary-menu) - The class on the
navor other element that wraps the menu (main-navigationin this article)
Replace .nav-menu and .main-navigation in the code below with the class names you found.
If you output the menu from your own theme, register a menu location in functions.php and call wp_nav_menu() from a template.
<?php
add_action( 'after_setup_theme', function () {
register_nav_menus( array( 'primary' => 'Main menu' ) );
} );
function my_theme_primary_nav() {
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class' => 'main-navigation',
'menu_class' => 'nav-menu',
) );
}
Call my_theme_primary_nav(); in header.php where you want the menu to appear.
Step 3: Build the panel and columns with CSS
Add the CSS to your child theme's style.css, or to Appearance > Customize > Additional CSS. It is written mobile first, and turns submenus into dropdowns and the mega menu panel only when the screen is 769px wide or more.
/* Shared (mobile first): submenus start closed and open with is-open */
.nav-menu,
.nav-menu .sub-menu {
list-style: none;
margin: 0;
padding: 0;
}
.nav-menu .sub-menu {
display: none;
}
.nav-menu li.is-open > .sub-menu {
display: block;
}
/* Third-level mega menu items are always shown while the panel is open */
.nav-menu > li.has-mega-menu .sub-menu .sub-menu {
display: block;
padding-left: 1rem;
}
.mega-menu-desc {
margin: 0.25rem 0 0.5rem;
font-size: 0.85rem;
color: #50575e;
}
/* Desktop (769px and wider) */
@media (min-width: 769px) {
/* The element the panel width is based on */
.main-navigation {
position: relative;
}
.nav-menu {
display: flex;
gap: 1.5rem;
}
.nav-menu > li {
position: relative;
}
/* Regular dropdowns */
.nav-menu .sub-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 100;
min-width: 200px;
padding: 0.5rem 1rem;
background: #fff;
border: 1px solid #dcdcde;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-menu li:hover > .sub-menu,
.nav-menu li:focus-within > .sub-menu {
display: block;
}
/* Mega menu: make the parent item static so the panel spans .main-navigation */
.nav-menu > li.has-mega-menu {
position: static;
}
.nav-menu > li.has-mega-menu > .sub-menu {
right: 0;
padding: 1.5rem 2rem;
}
.nav-menu > li.has-mega-menu:hover > .sub-menu,
.nav-menu > li.has-mega-menu:focus-within > .sub-menu,
.nav-menu > li.has-mega-menu.is-open > .sub-menu {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1.5rem;
}
/* Column headings (second level) */
.nav-menu > li.has-mega-menu > .sub-menu > li > a {
display: block;
margin-bottom: 0.5rem;
font-weight: 700;
border-bottom: 1px solid #dcdcde;
}
/* Links inside a column (third level) sit in the panel as is */
.nav-menu > li.has-mega-menu .sub-menu .sub-menu {
position: static;
min-width: 0;
padding: 0;
background: none;
border: 0;
box-shadow: none;
}
}
Three key points:
- What the panel width is based on: an element with
position: absoluteis positioned relative to its nearest ancestor whosepositionis notstatic. Making the parentlistaticand.main-navigationrelativelines the panel's left and right edges up with the whole menu. To stretch it from edge to edge of the screen, putposition: relativeon the element that wraps the entire header. - Switching display: regular dropdowns use
display: block, and only the mega menu panel usesdisplay: grid. The mega menu selectors are more specific, so they win. - Keyboard use: because
:focus-withinis included, the panel also opens when you Tab to the parent item. With:hoveralone it does not open from the keyboard.
Some common snags when testing:
- If any parent element has
overflow: hidden, the panel is clipped and disappears. Check the ancestors one by one in the developer tools. - The top of the panel lines up with the bottom of
.main-navigation. If there is a gap between the menu item and the panel,:hoveris lost as the mouse moves across it and the panel closes. If you get a gap, move the bottom spacing of.main-navigationintopaddingon theli. - If your theme's own menu CSS (
displayorposition) is still in effect, it conflicts with these rules. Look for overridden declarations in the developer tools and adjust.
Step 4: Add an open/close button for phones and touch
On touch devices :hover is unreliable, and on phones the CSS above keeps the panel closed. Add JavaScript that inserts an open/close button next to the parent item and toggles the is-open class when pressed.
Create js/mega-menu.js in your child theme.
document.querySelectorAll('.nav-menu > li.has-mega-menu').forEach(function (item) {
var link = item.querySelector(':scope > a');
var panel = item.querySelector(':scope > .sub-menu');
if (!link || !panel) {
return;
}
var button = document.createElement('button');
button.type = 'button';
button.className = 'mega-menu-toggle';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-label', link.textContent.trim() + ' submenu toggle');
button.textContent = '+';
link.after(button);
button.addEventListener('click', function () {
var open = item.classList.toggle('is-open');
button.setAttribute('aria-expanded', open ? 'true' : 'false');
button.textContent = open ? '-' : '+';
});
});
// Close open panels with the Esc key
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') {
return;
}
document.querySelectorAll('.nav-menu > li.has-mega-menu.is-open').forEach(function (item) {
item.classList.remove('is-open');
var button = item.querySelector(':scope > .mega-menu-toggle');
if (button) {
button.setAttribute('aria-expanded', 'false');
button.textContent = '+';
}
});
});
Load it from your child theme's functions.php. Passing strategy in an array as the fifth argument of wp_enqueue_script() works in WordPress 6.3 and later (and is the same in the current version as of November 2026).
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'my-mega-menu',
get_stylesheet_directory_uri() . '/js/mega-menu.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
} );
Esc closes panels that were opened with the button (those with is-open). While the mouse is over the panel or focus is inside it, the CSS :hover and :focus-within rules keep it open.
Step 5: Show descriptions under column headings (optional)
Text entered in a menu item's "Description" field is often not output by themes. The walker_nav_menu_start_el filter lets you append text after a menu item's HTML. The code below shows the description only for second-level items ($depth counts from 0, so 1) in the menu assigned to the primary location.
<?php
add_filter( 'walker_nav_menu_start_el', function ( $item_output, $menu_item, $depth, $args ) {
if ( 'primary' !== $args->theme_location || 1 !== $depth || '' === trim( $menu_item->description ) ) {
return $item_output;
}
return $item_output . '<p class="mega-menu-desc">' . esc_html( $menu_item->description ) . '</p>';
}, 10, 4 );
primary is the name of the menu location your theme registers. It differs by theme, so check the register_nav_menus() call in your theme's functions.php or elsewhere and replace it.
You can also write your own walker that extends Walker_Nav_Menu, but if you only need to add classes or text, the CSS class and filter in this article are enough. Consider a custom walker when you want to change the HTML structure itself, such as putting images or widgets in a column.
Block themes and plugins
- Block themes: in block themes such as Twenty Twenty-Four, menus are built with the Navigation block in the Site Editor. If the theme does not register menu locations, Appearance > Menus does not appear at all. The Navigation block outputs different HTML and class names from those in this article, so the CSS and JavaScript above will not work as is.
- Plugins: if you want images or widgets inside columns, or want to set the number and width of columns in the admin screen, a mega menu plugin is an option. For example, Max Mega Menu, distributed on wordpress.org, is described as working with block themes as of November 2026. Before installing a plugin, check on a test site that it does not conflict with your theme's menu CSS.
Basic menu creation and display tweaks are covered in "How to customize WordPress navigation menus".
Summary
- In a classic theme, add the
has-mega-menuclass to the parent item under Appearance > Menus, and arrange the second level as column headings and the third level as links. - In CSS, set the parent
litoposition: staticand the element wrapping the menu toposition: relative, then split the panel into columns withdisplay: grid. Include:focus-withinso it opens from the keyboard. - For phones and touch devices, add a JavaScript open/close button that toggles the
is-openclass. - Block themes use a different HTML structure, so the code in this article does not work there as is.
References: wp_nav_menu(), walker_nav_menu_start_el, wp_enqueue_script(), MDN – :focus-within
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)