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-menu to 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

  1. Go to Appearance > Menus in the admin screen.
  2. Open "Screen Options" at the top right and check "CSS Classes". If you want to show a description under column headings, also check "Description".
  3. Open the first-level item you want to turn into a mega menu and enter has-mega-menu in "CSS Classes (optional)".
  4. 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-menu in this article; some themes use menu or primary-menu)
  • The class on the nav or other element that wraps the menu (main-navigation in 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: absolute is positioned relative to its nearest ancestor whose position is not static. Making the parent li static and .main-navigation relative lines the panel's left and right edges up with the whole menu. To stretch it from edge to edge of the screen, put position: relative on the element that wraps the entire header.
  • Switching display: regular dropdowns use display: block, and only the mega menu panel uses display: grid. The mega menu selectors are more specific, so they win.
  • Keyboard use: because :focus-within is included, the panel also opens when you Tab to the parent item. With :hover alone 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, :hover is lost as the mouse moves across it and the panel closes. If you get a gap, move the bottom spacing of .main-navigation into padding on the li.
  • If your theme's own menu CSS (display or position) 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-menu class 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 li to position: static and the element wrapping the menu to position: relative, then split the panel into columns with display: grid. Include :focus-within so it opens from the keyboard.
  • For phones and touch devices, add a JavaScript open/close button that toggles the is-open class.
  • 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)

お気軽にご相談ください

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