Mega menus are widely used in web design, and with the advent of block themes, I’ve been looking for a way to incorporate them seamlessly into WordPress’s Navigation block. The upcoming release of WordPress 6.5 at the end of March includes features like the Interactivity API that will help finally bring block-based mega menus to life. In this article, I’ll walk you through one approach using these new tools.
Now, only some sites need a mega menu. If this tutorial doesn’t seem relevant to your workflow on its surface, I still encourage you to give it a read. The article is more about architecting a block plugin using new functionality in WordPress 6.5. Many concepts we will cover apply well beyond mega menus. Here are a few examples:
- How to create custom template part areas
- How to add custom blocks to the Navigation block
- How to set up a project that uses the Interactivity API
- How to use Core components to streamline block development
Before diving in, here’s a look at the result using the Twenty Twenty-Four theme. We’ll not be building a production-ready block to keep this article from going too long, but it will provide a solid foundation for continued iterations.
Table of Contents
The approach
There are many approaches you could take when building a Mega Menu block, so before we begin, let’s look at the prerequisites I had when structuring this project.
- The Mega Menu block needs to integrate directly with the Navigation block
- It should be the same experience as adding any other link
- Once a user adds a Mega Menu block to the Navigation block, they then choose from a list of available “menu templates” to display on the front end
- Menus themselves are template parts
- Menu template parts are created and designed in the Site Editor
I took as much inspiration as possible from Core, and the resulting block closely resembles the Navigation Link block. The more the block feels like native WordPress, the better.
Getting set up
The first step is to scaffold a block plugin using the @wordpress/create-block
package. I’m not going to go into too much detail here, but you can refer to the Getting Started documentation to learn more about this process.
The following command will create a plugin that supports using wp-env
and registers the dynamic block mega-menu-block
. Feel free to use your preferred plugin slug and local development environment; wp-env
is not required. You just need to make sure you’re running WordPress 6.5.
npx @wordpress/create-block@latest mega-menu-block --variant=dynamic --wp-env
cd mega-menu-block
Throughout this tutorial, all edits will be made to the files in the plugin’s /src
folder unless otherwise indicated.
Adding a custom template part area
Before configuring the block itself, let’s register the template part area that will house each mega menu. You can add custom areas using the default_wp_template_part_areas
hook.
/**
* Adds a custom template part area for mega menus to the list of template part areas.
*
* @param array $areas Existing array of template part areas.
* @return array Modified array of template part areas including the new "Menu" area.
*/
function outermost_mega_menu_template_part_areas( array $areas ) {
$areas[] = array(
'area' => 'menu',
'area_tag' => 'div',
'description' => __( 'Menu templates are used to create sections of a mega menu.', 'mega-menu-block' ),
'icon' => '',
'label' => __( 'Menu', 'mega-menu-block' ),
);
return $areas;
}
add_filter( 'default_wp_template_part_areas', 'outermost_mega_menu_template_part_areas' );
Place this PHP code in the main plugin file in the root mega-menu-block
folder. It should be mega-menu-block.php
unless you choose a different block slug. Note that the area
is set to menu
. We’ll use this later in the tutorial.
In your local environment, navigate to the Site Editor. You should see that the “Menu” area is now selectable when creating a new template part.
As of WordPress 6.5, there is no way to assign a custom icon to template part areas. The options are header
, footer
, and sidebar
. Leaving the field blank or specifying any other value will display a default icon, as seen in the image above.
Create a new Menu template part and add some filler content. I chose to insert one of the patterns from the Twenty Twenty-Four theme. Don’t worry too much about what it looks like. We just need a saved template for testing purposes.
Adding mega menus to the Navigation block
It’s now time to start building out the “Mega Menu Block” block, and the first thing to do is ensure users can add it to the Navigation block in WordPress.
Start the build process by running npm start
in the terminal. Navigate to a new page in your local environment and confirm that the block is available in the Editor. It shouldn’t look like much yet, just the default block that’s scaffolded by the create-block
package.
Now add a Navigation block to the page and try adding the Mega Menu Block in the menu. You won’t be able to.
By default, the Navigation block only permits a predefined set of Core blocks, controlled by an array of block names defined in the block’s allowedBlocks
setting. However, you can now add support for custom blocks using blocks.registerBlockType
filter in WordPress 6.5.
The filter itself is not new. It loops through each block type and allows you to modify the settings of each, and you may have seen it before in other tutorials here on the Developer Blog. The filter’s callback function accepts two parameters: an object of block settings (blockSettings
) and the block’s name (blockName
).
To use the filter, start by importing addFilter
at the top of the index.js
file.
import { addFilter } from '@wordpress/hooks';
Then, add the following code at the bottom of the file and save. The build process should still be running. If not, run npm start
in the terminal.
/**
* Make the Mega Menu Block available to Navigation blocks.
*
* @param {Object} blockSettings The original settings of the block.
* @param {string} blockName The name of the block being modified.
* @return {Object} The modified settings for the Navigation block or the original settings for other blocks.
*/
const addToNavigation = ( blockSettings, blockName ) => {
if ( blockName === 'core/navigation' ) {
return {
...blockSettings,
allowedBlocks: [
...( blockSettings.allowedBlocks ?? [] ),
'create-block/mega-menu-block',
],
};
}
return blockSettings;
};
addFilter(
'blocks.registerBlockType',
'add-mega-menu-block-to-navigation',
addToNavigation
);
Before modifying any settings, it’s important to ensure we only target the Navigation block by checking the blockName
. Then, append create-block/mega-menu-block
to the allowedBlocks
setting. This is the name
of the block as defined in block.json
.
After saving the file and refreshing the page, you should now be able to add the Mega Menu Block to menus.
Updating block.json, block styles, and adding a custom icon
For the sake of this tutorial, I am going to keep the Editor functionality of the Mega Menu Block to the basics. You can always extend it, and I encourage you to do so.
In the Editor, the block requires two features: a way to set a label for the menu item within the Navigation block and a selection mechanism for choosing a menu template part for the mega menu. The label and selected template part data must be stored as block attributes.
We’ll begin by updating the block.json
file to include these attributes and do some additional cleanup. Here’s the todo list:
- Add an
attributes
property - Add a
label
attribute with typestring
- Add a
menuSlug
attribute with typestring
- Add a
parent
property and set it tocore/navigation
so users cannot insert the block outside of a Navigation block - Add typography support to match the other link blocks available in the Navigation block
- Update the
title
property to “Mega Menu” - Update the
description
property to “Add a mega menu to your navigation.” - Update the
category
property to “design” - Remove the
icon
property (we’ll add a custom icon later)
The updated block.json
file should look like this.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "create-block/mega-menu-block",
"version": "0.1.0",
"title": "Mega Menu",
"category": "design",
"description": "Add a mega menu to your navigation.",
"parent": [ "core/navigation" ],
"example": {},
"attributes": {
"label": {
"type": "string"
},
"menuSlug": {
"type": "string"
}
},
"supports": {
"html": false,
"typography": {
"fontSize": true,
"lineHeight": true,
"__experimentalFontFamily": true,
"__experimentalFontWeight": true,
"__experimentalFontStyle": true,
"__experimentalTextTransform": true,
"__experimentalTextDecoration": true,
"__experimentalLetterSpacing": true,
"__experimentalDefaultControls": {
"fontSize": true
}
}
},
"textdomain": "mega-menu-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php",
"viewScript": "file:./view.js"
}
Next, clear out the default styles from the create-block
setup by removing all content in the style.scss
and edit.scss
files. We’ll introduce custom styling later in this tutorial. Remember to save the changes to both files.
This final clean-up step is unnecessary, but I always like adding a custom block icon. The icon
property in block.json
allows you to specify a Dashicon slug, but you cannot add SVG icons this way. Instead, let’s add the icon directly to the registerBlockType()
function in the index.js
file.
const megaMenuIcon = (
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M20,12 L4,12 L4,13.5 L20,13.5 L20,12 Z M10,6.5 L4,6.5 L4,8 L10,8 L10,6.5 Z M20,17.5 L4,17.5 L4,19 L20,19 L20,17.5 Z M20,5.62462724 L16.000015,9 L12,5.62462724 L12.9791165,4.5 L16.000015,7.04920972 L19.0208935,4.5 L20,5.62462724 Z"></path>
</svg>
);
registerBlockType( metadata.name, {
icon: megaMenuIcon,
edit: Edit,
} );
Save the file and refresh the page. The “Mega Menu” block should look like this in the Editor. Notice the Typography panel provided by the block supports.
Adding the Editor user interface
With the initial setup complete, it is now time to add controls to the block that allow the user to set the label
and menuSlug
attributes.
In the edit.js
file, let’s start by updating the Edit
component to include the properties attributes
and setAttributes
. From attributes
, extract label
and menuSlug
. we’ll use setAttributes
later to update the values based on user interaction.
Finally, the markup in the Editor defaults to using a <p>
tag. Update that to a <div>
. The results should look like this.
export default function Edit( { attributes, setAttributes } ) {
const { label, menuSlug } = attributes;
return (
<div { ...useBlockProps() }>
{ __(
'Mega Menu Block – hello from the editor!',
'mega-menu-block'
) }
</div>
);
}
Importing components, hooks, and functions
Next, we need to import a few items that will be used to build the block interface. For the sake of brevity, let’s add them all at once. We’ll need:
InspectorControls
: A component that renders block-specific settings in the sidebar.PanelBody
: A component used withinInspectorControls
to group related UI controls in a collapsible container for better organization.TextControl
: A form input component that allows users to enter and edit text.ComboboxControl
: A combined input and dropdown menu component that allows users to choose from predefined options.RichText
: A component that provides a rich text editing interface.useEntityRecords
: A React hook that retrieves a list of entities (e.g., posts, pages, template parts) from the WordPress database based on specified query parameters.
Update the imports at the top of the edit.js
file to include the following.
import { __ } from '@wordpress/i18n';
import { InspectorControls, RichText, useBlockProps } from '@wordpress/block-editor';
import { ComboboxControl, PanelBody, TextControl } from '@wordpress/components';
import { useEntityRecords } from '@wordpress/core-data';
Fetching menu template parts
We’ve included essential imports, and the block now has access to the label
and menuSlug
attributes. The one missing piece of information is the available “Menu” template parts.
Let’s use the useEntityRecords
hook to fetch all entities of the type wp_template_part
and then parse the returned records for all template parts with the area menu
, as defined earlier in this tutorial. The code should be added before the return statement in the Edit
component and should look something like this.
// Fetch all template parts.
const { hasResolved, records } = useEntityRecords(
'postType',
'wp_template_part',
{ per_page: -1 }
);
let menuOptions = [];
// Filter the template parts for those in the 'menu' area.
if ( hasResolved ) {
menuOptions = records
.filter( ( item ) => item.area === 'menu' )
.map( ( item ) => ( {
label: item.title.rendered, // Title of the template part.
value: item.slug, // Template part slug.
} ) );
}
Note that we can retrieve all records by setting per_page
to -1
.
The hasResolved
variable indicates whether the request to fetch the template parts has been completed. Once the fetching process has resolved (hasResolved
is true
), the code filters through the records
(the fetched template parts) to find those that belong to the menu
area.
For each template part in the menu
area, the code constructs an object containing the template part’s title and slug. These objects are collected into the menuOptions
array, which we’ll then use to represent options in a ComboboxControl
component.
For more information on fetching entity records, check out the article useEntityRecords: an easier way to fetch WordPress data.
Adding the Settings panel
We have all the data needed to build out the settings panel for the block. To do so, let’s start by adding an InspectorControls
component within the return statement. Then add a PanelBody
component with the title
property set to “Settings”. Core blocks generally have setting panels open by default, so set the initialOpen
property to true
.
The updated return statement of the Edit
component should look like this:
return (
<>
<InspectorControls>
<PanelBody
title={ __( 'Settings', 'mega-menu-block' ) }
initialOpen={ true }
>
Testing
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>
{ __(
'Mega Menu Block – hello from the editor!',
'mega-menu-block'
) }
</div>
</>
);
In React, a component can only return a single element, which is why everything is wrapped in a Fragment (<>...</>
) in the code above.
Save the edit.js
file and preview the Mega Menu block in the Editor. You should see a “Settings” panel when the block is selected.
Next, let’s use the TextControl
component to allow users to modify the label
attribute and the ComboboxControl
component to choose a menu template and set the menuSlug
attribute.
<PanelBody
title={ __( 'Settings', 'mega-menu-block' ) }
initialOpen={ true }
>
<TextControl
label={ __( 'Label', 'mega-menu-block' ) }
type="text"
value={ label }
onChange={ ( value ) =>
setAttributes( { label: value } )
}
autoComplete="off"
/>
<ComboboxControl
label={ __( 'Menu Template', 'mega-menu-block' ) }
value={ menuSlug }
options={ menuOptions }
onChange={ ( slugValue ) =>
setAttributes( { menuSlug: slugValue } )
}
/>
</PanelBody>
Note that we are using setAttributes
to update the values of both label
and menuSlug
based on user interaction.
After saving the edit.js
file, the controls will be available in the Settings panel. Try modifying the Label and selecting a Menu Template. Confirm that the values are saved when updating the page.
While beyond the scope of this tutorial, if you plan to distribute this block to users, you will want to add some sort of notice if no menu template parts exist. Perhaps also provide a link that directs them to the Site Editor to create new templates.
Adding a RichText field in the canvas
While the label
attribute is editable in the Settings Sidebar, this is not a great user experience. If you look at the code for the Navigation Link block in WordPress, you will see that the label is also editable using a RichText
component in the Editor canvas.
Editing the Mega Menu block should feel as much like native WordPress as possible, and we don’t want to reinvent the wheel. Therefore, copy the same markup structure and CSS classes in the Navigation Link block and implement the RichText
component. This allows our block to inherit Core styles and provide a consistent user interface.
<div { ...useBlockProps() }>
<a className="wp-block-navigation-item__content">
<RichText
identifier="label"
className="wp-block-navigation-item__label"
value={ label }
onChange={ ( labelValue ) =>
setAttributes( {
label: labelValue,
} )
}
aria-label={ __(
'Mega menu link text',
'mega-menu-block'
) }
placeholder={ __( 'Add label…', 'mega-menu-block' ) }
allowedFormats={ [
'core/bold',
'core/italic',
'core/image',
'core/strikethrough',
] }
/>
</a>
</div>
Here’s a look at the RichText
component once the above code is applied.
The Editor component for the Mega Menu block provides the basic functionality we need and is now complete. Let’s shift focus to the front end.
View the complete edit.js file
/**
* Retrieves the translation of text.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
import {
InspectorControls,
RichText,
useBlockProps,
} from '@wordpress/block-editor';
import { ComboboxControl, PanelBody, TextControl } from '@wordpress/components';
import { useEntityRecords } from '@wordpress/core-data';
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './editor.scss';
/**
* The edit function describes the structure of your block in the context of the
* editor. This represents what the editor will render when the block is used.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
*
* @param {Object} props Properties passed to the function.
* @param {Object} props.attributes Available block attributes.
* @param {Function} props.setAttributes Function that updates individual attributes.
*
* @return {Element} Element to render.
*/
export default function Edit( { attributes, setAttributes } ) {
const { label, menuSlug } = attributes;
// Fetch all template parts.
const { hasResolved, records } = useEntityRecords(
'postType',
'wp_template_part',
{ per_page: -1 }
);
let menuOptions = [];
// Filter the template parts for those in the 'menu' area.
if ( hasResolved ) {
menuOptions = records
.filter( ( item ) => item.area === 'menu' )
.map( ( item ) => ( {
label: item.title.rendered,
value: item.slug,
} ) );
}
return (
<>
<InspectorControls>
<PanelBody
title={ __( 'Settings', 'mega-menu-block' ) }
initialOpen={ true }
>
<TextControl
label={ __( 'Label', 'mega-menu-block' ) }
type="text"
value={ label }
onChange={ ( value ) =>
setAttributes( { label: value } )
}
autoComplete="off"
/>
<ComboboxControl
label={ __( 'Menu Template', 'mega-menu-block' ) }
value={ menuSlug }
options={ menuOptions }
onChange={ ( slugValue ) =>
setAttributes( { menuSlug: slugValue } )
}
/>
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>
<a className="wp-block-navigation-item__content">
<RichText
identifier="label"
className="wp-block-navigation-item__label"
value={ label }
onChange={ ( labelValue ) =>
setAttributes( { label: labelValue } )
}
aria-label={ __(
'Mega menu link text',
'mega-menu-block'
) }
placeholder={ __( 'Add label…', 'mega-menu-block' ) }
allowedFormats={ [
'core/bold',
'core/italic',
'core/image',
'core/strikethrough',
] }
/>
</a>
</div>
</>
);
}
Configuring the front end
Following the steps above, the front end of the Mega Menu block should look like this.
The block is correctly displayed as part of the Navigation block, but the default output remains. Let’s fix this.
Updating the block markup and base styles
Navigate to the render.php
file and assign the label
and menuSlug
attributes to variables. Add a check that returns null
if neither exists. We don’t want to display a mega menu without a label or a label without a mega menu.
Finally, replace the default message with the menu label.
<?php
$label = esc_html( $attributes['label'] ?? '' );
$menu_slug = esc_attr( $attributes['menuSlug'] ?? '');
// Don't display the mega menu link if there is no label or no menu slug.
if ( ! $label || ! $menu_slug ) {
return null;
}
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
<?php echo $label; ?>
</p>
The Navigation block is an unordered list (<ul>
), so next, update the markup to ensure the block renders as a list item (<li>
). The menu label should also be contained in a <button>
element that toggles the mega menu when clicked.
<li <?php echo get_block_wrapper_attributes(); ?>>
<button><?php echo $label; ?></button>
</li>
Browsers provide default styles to <button>
elements, which we don’t want. Let’s add a few reset styles in the style.scss
file.
// Reset button styles.
.wp-block-create-block-mega-menu-block {
button {
background-color: initial;
border: none;
color: currentColor;
cursor: pointer;
font-family: inherit;
font-size: inherit;
font-style: inherit;
font-weight: inherit;
line-height: inherit;
padding: 0;
text-transform: inherit;
}
}
Note that the main class for the block will be wp-block-create-block-mega-menu-block
. WordPress generates this automatically using the get_block_wrapper_attributes()
function. The block name
is converted to kebab case and prefixed with wp-block-
.
Save both the render.php
and style.scss
files. When you refresh the page on the front end, it should look something like this.
Adding the mega menu
It’s time to render the mega menu template part using the block_template_part()
function, which accepts the $menu_slug
variable. To make subsequent steps easier, wrap this function in a <div>
with the wp-block-create-block-mega-menu-block__menu-container
class.
<li <?php echo get_block_wrapper_attributes(); ?>>
<button><?php echo $label; ?></button>
<div class="wp-block-create-block-mega-menu-block__menu-container">
<?php echo block_template_part( $menu_slug ); ?>
</div>
</li>
The last step is adding a <button>
element inside the menu container. Users will be able to click the button to hide the mega menu. This button could be text or an icon. I decided to use the close
icon from the WordPress component library.
The render.php
file should now look like this.
<?php
$label = esc_html( $attributes['label'] ?? '' );
$menu_slug = esc_attr( $attributes['menuSlug'] ?? '');
$close_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>';
// Don't display the mega menu link if there is no label or no menu slug.
if ( ! $label || ! $menu_slug ) {
return null;
}
?>
<li <?php echo get_block_wrapper_attributes(); ?>>
<button><?php echo $label; ?></button>
<div class="wp-block-create-block-mega-menu-block__menu-container">
<?php echo block_template_part( $menu_slug ); ?>
<button
aria-label="<?php echo __( 'Close menu', 'mega-menu' ); ?>"
class="menu-container__close-button"
type="button"
>
<?php echo $close_icon; ?>
</button>
</div>
</li>
Refresh the page, and you will see that the template part and the close button render on the front end. It doesn’t look great, but we’ll fix that with the Interactivity API and more styling.
Adding interactions (Interactivity API)
To manage the behavior of our mega menu, we’ll leverage the Interactivity API. This section won’t cover every detail of the API, but it should give you a good foundation for how interactive blocks are structured and function.
I drew inspiration from the Navigation block when experimenting with block-based mega menus. The Interactivity API powers many parts of it, and the source code provides a good template for the level of interaction you would need in a production-ready block.
For this tutorial, we’re just going to cover the basics. When a user clicks on the menu item, it should toggle the mega menu. If we wanted to cover everything needed to build a block that responds to clicks, hovers, and focus states while being fully responsive, we’d need a much longer guide.
There are three things that we need to do:
- Update block and plugin to support the Interactivity API
- Add directives to the markup on the front end to enable specific interactions within the block
- Create a store that contains the logic (state, actions, or callbacks) for the desired interactivity
Adding support for the Interactivity API
Our build process relies on wp-scripts
, introduced during the create-block
scaffolding process. We need to adjust a couple of things to accommodate the Interactivity API.
Stop the build process if it’s currently running. Next, open the package.json
file and add the --experimental-modules
flag to both the build
and start
scripts. The adjustments should look like this.
"scripts": {
"build": "wp-scripts build --webpack-copy-php --experimental-modules",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"lint:js:src": "wp-scripts lint-js ./src --fix",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
"start": "wp-scripts start --webpack-copy-php --experimental-modules",
"env": "wp-env"
},
In the block.json
file, change the viewScript
property to viewScriptModule
and save.
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php",
"viewScriptModule": "file:./view.js"
}
Restart the build process by running npm start
in the terminal and confirm that all changes are applied correctly. On the front end, refresh the page and check your browser’s console. You should see the message:
Hello World! (from create-block-mega-menu-block block)
The Interactivity API script requires new modules in WordPress 6.5, so blocks must enqueue any JavaScript that relies on the API by using viewScriptModule
instead of viewScript
. Considering this module requirement, the --experimental-modules
flag tells wp-scripts
how to build view.js
properly.
While the flag is technically experimental, it only impacts the build process of the block plugin and is safe to use. Once the plugin is built (npm run build
) and production-ready, it does not rely on this experimental flag in any way.
Finally, to indicate that the block supports the Interactivity API, add "interactivity": true
to the supports
section of the block’s block.json
file.
"supports": {
"html": false,
"interactivity": true,
"typography": {
...
}
},
Refer to the Block Editor Handbook for a more detailed description of the `interactivity` support property.
Adding directives
Directives are custom attributes added to the block’s markup that enable “interactions”. Interactivity API directives use the data-
prefix.
Here is a list of the directives we’ll need for the Mega Menu block. Follow the links for code examples and more information about each:
wp-interactive
: Enables interactivity for the DOM element and its childrenwp-context
: Defines a local state available to the DOM element and its childrenwp-bind
: Sets HTML attributes on elements based on a boolean or string valuewp-on
: Runs code on dispatched DOM events (click
,focusout
,keydown
, etc.)
The wp-interactive
directive is always required and accepts a namespace. Using the block name is good practice unless you require a more advanced implementation. Add this directive to the main HTML element of the block.
<li
<?php echo $wrapper_attributes; ?>
data-wp-interactive="create-block/mega-menu-block"
>
...
</li>
Next, add the wp-context
directive to the <li>
element as well. We’ll use this to track the state of the mega menu. Is it open or closed?
For this state, let’s use a variable called isMenuOpen
with an initial state set to false
. The directive accepts stringified JSON as a value, so it should look like this.
<li
<?php echo $wrapper_attributes; ?>
data-wp-interactive="create-block/mega-menu-block"
data-wp-context='{ "isMenuOpen": false }'
>
...
</li>
Let’s think about how the mega menu should function.
If a user clicks the menu label <button>
element for the first time, display the mega menu. This action should set isMenuOpen
to true
. If the user clicks the button again, and isMenuOpen
is true
, hide the menu and set isMenuOpen
to false
.
To handle this interaction, let’s add a wp-on
directive for a click
event, which looks like data-wp-on–click
. The directive accepts a callback that gets executed each time the associated event is triggered. We’ll create this later in this tutorial. For now, set the directive equal to actions.toggleMenu
.
<li
<?php echo $wrapper_attributes; ?>
data-wp-interactive="create-block/mega-menu-block"
data-wp-context='{ "isMenuOpen": false }'
>
<button
data-wp-on--click="actions.toggleMenu"
>
<?php echo $label; ?>
</button>
...
</li>
Next, let’s use a wp-bind
to set the attribute aria-expanded=true
if isMenuOpen
is true
. This will control the visibility of the mega menu in combination with some custom styles, which we’ll add later.
<li
<?php echo $wrapper_attributes; ?>
data-wp-interactive="create-block/mega-menu-block"
data-wp-context='{ "isMenuOpen": false }'
>
<button
data-wp-on--click="actions.toggleMenu"
data-wp-bind--aria-expanded="context.isMenuOpen"
>
<?php echo $label; ?>
</button>
...
</li>
You could take other approaches besides using the aria-expanded
attribute, such as adding a custom class using the wp-class
directive.
The last directive to add is for the <button>
element within the mega menu that, when clicked, will close it. Again, let’s use the wp-on
directive, but we’ll pass the action.closeMenu
callback instead of action.toggleMenu
.
The complete block markup, with directives, should look like this.
<li
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="create-block/mega-menu-block"
data-wp-context='{ "isMenuOpen": false }'
>
<button
data-wp-on--click="actions.toggleMenu"
data-wp-bind--aria-expanded="context.isMenuOpen"
>
<?php echo $label; ?>
</button>
<div class="wp-block-create-block-mega-menu-block__menu-container">
<?php echo block_template_part( $menu_slug ); ?>
<button
aria-label="<?php echo __( 'Close menu', 'mega-menu' ); ?>"
class="menu-container__close-button"
type="button"
data-wp-on--click="actions.closeMenu"
>
<?php echo $close_icon; ?>
</button>
</div>
</li>
Adding the store
At this point, the directives don’t do anything. Nothing will have changed if you save render.php
and look at the front end. You must create a store that defines the interactions specified in the directives, specifically action.toggleMenu
and action.openMenu
.
Start by opening the view.js
file and remove the default console statement. Then, import the store
from the @wordpress/interactivity
package. Let’s also import getContext
. This will allow us to get the context of the block and determine the current value of isMenuOpen
.
import { store, getContext } from '@wordpress/interactivity';
The store()
in this tutorial is quite basic. All we need to do is create the actions that will toggle and close the mega menu, which you can do by setting the value of isMenuOpen
to true
or false
.
The store()
accepts the namespace defined using the wp-interactivity
directive and an object containing actions, states, callbacks, and more. This is where we will define action.toggleMenu
and action.openMenu
.
The complete view.js
file should look something like this.
/**
* WordPress dependencies
*/
import { store, getContext } from '@wordpress/interactivity';
const { actions } = store( 'create-block/mega-menu-block', {
actions: {
toggleMenu() {
const context = getContext();
if ( context.isMenuOpen ) {
actions.closeMenu();
} else {
context.isMenuOpen = true;
}
},
closeMenu() {
const context = getContext();
context.isMenuOpen = false;
},
}
} );
You can learn more about structuring a store in the official documentation.
Adding styles
The final step is adding custom styles. They must hide the mega menu by default, display it when aria-expanded=true
on the primary <button>
element, set the position and width of the menu content, and style the close button.
Open the style.scss
file and update it to match the following.
.wp-block-create-block-mega-menu-block {
// Reset button styles.
button {
background-color: initial;
border: none;
color: currentColor;
cursor: pointer;
font-family: inherit;
font-size: inherit;
font-style: inherit;
font-weight: inherit;
line-height: inherit;
padding: 0;
text-transform: inherit;
}
.wp-block-create-block-mega-menu-block__menu-container {
height: auto;
right: 0;
opacity: 0;
overflow: hidden;
position: absolute;
top: 40px;
transition: opacity .1s linear;
visibility: hidden;
width: var(--wp--style--global--wide-size);
z-index: 2;
.menu-container__close-button {
align-items: center;
-webkit-backdrop-filter: blur(16px) saturate(180%);
backdrop-filter: blur(16px) saturate(180%);
background-color: #ffffffba;
border: none;
border-radius: 999px;
cursor: pointer;
display: flex;
justify-content: center;
opacity: 0;
padding: 4px;
position: absolute;
right: 12px;
text-align: center;
top: 12px;
transition: opacity .2s ease;
z-index: 100;
// Show the close button when focused (for keyboard navigation)
&:focus {
opacity: 1;
}
}
// Show the close button when the mega menu is hovered.
&:hover {
.menu-container__close-button {
opacity: 1;
}
}
}
// Show the mega menu when aria-expanded is true.
button[aria-expanded=true] {
&~.wp-block-create-block-mega-menu-block__menu-container {
opacity: 1;
overflow: visible;
visibility: visible;
}
}
}
I am not going to go through all of this code. Styling the mega menu is the most challenging part of building this block.
From the menu’s position and the content’s width to how the menu looks on mobile, there are many factors to account for. Furthermore, every website design is different, and each will have different requirements. I encourage you to experiment and make it your own.
That said, this is what the Mega Menu block looks like in its current state.
Next steps
Alright, so this is a good point to stop. While the result of this tutorial is far from a production-ready block, it provides a solid framework to iterate on. If you are interested in taking this block further, here are some issues that need to be fixed and enhancements that would improve the block.
- The mega menu position should adapt to the position of the Navigation block and the size of the browser window
- Allow the user to configure the width of each mega menu and its position in relation to the Navigation block
- Add support for focus states, keyboard navigation, and addition accessibility features
- Add support for vertically positioned Navigation blocks
- Add mobile and tablet support
- Add title and description attributes to the menu item for parity with other link blocks
- Add an icon to the menu item to indicate it opens a mega menu, much like links with submenus
- Improve the user experience in the Editor
For a more complete example, you can check out my experimental Mega Menu block, which this tutorial is based on. It’s still far from production-ready but tackles many items on the list above.
There are many ways you could build a Mega Menu block. The approach taken in this tutorial tries to integrate with the Navigation block and provide a native WordPress experience as much as possible. Furthermore, the use of template parts decouples the design of mega menus from the Navigation block, which I find works well.
But if you have explored alternative approaches, please share in the comments. Mega menus are one of the most requested features in WordPress, and getting them right is a rewarding challenge.
Props to @greenshady and @bjmcsherry for providing feedback and reviewing this post.
Leave a Reply