Creating WordPress Mega Menus using the Gutenberg Editor

Building a website with a user-friendly and scalable navigation system is crucial. This blog post dives into the technical aspects of our custom mega menu solution. We’ll explore how we leverage WordPress’s classic menus and integrate them with custom Gutenberg blocks to create a dynamic and manageable navigation experience.

Introduction

When it comes to building a scalable and modular CMS for our clients, our go-to recommendation often involves implementing a WordPress solution accompanied by custom Gutenberg blocks. This way, we develop a fast & secure CMS that our clients can easily manage and scale according to their needs.

WordPress itself comes with some built-in “core” blocks which are flexible and allow users to build simple layouts easily. Often these “core” blocks are not enough for building more advanced layouts & designs, therefore we need to extend Gutenberg editor’s functionality, adding custom blocks tailored to our client needs. This not only enhances their editing experience but also contributes to an elevated and streamlined content creation process.

We opt for a “Hybrid Theme” approach, where we leverage Gutenberg features, empowering our clients to effortlessly create new pages using the custom blocks and modules we build specifically for them, while ensuring alignment with brand guidelines, design system, and overall visual coherence.

Custom Mega Menus functionality, based on Gutenberg editor.

While managing page content is entirely in the client’s hands, the website’s header and footer are strategically implemented through code. This intentional separation enables clients to concentrate solely on page editing, ensuring a user-friendly experience while retaining full control over their content.

Implementing the header and footer through code doesn’t limit client control over their content.

For the website’s navigation, we are using the classic WordPress menus extended with custom functionality for mega menus that allow clients to populate mega menus content using Gutenberg blocks. Through that workflow, clients can easily populate mega menus and attach them to menu items.

How we developed custom mega menus functionality using Gutenberg blocks

In a nutshell, the mega menu logic is as follows: clients populate the mega menus on a custom post type created specifically for this purpose. Within the ‘mega menu’ post type they can use custom Gutenberg blocks that we develop for building up mega menu content. The populated mega menus can be then linked to a top-level menu item through a custom field. Finally, the custom WordPress Nav Menu Walker comes into play, retrieving the mega menu content and displaying it as a child element on that menu item.

Let’s take it step by step:

Step 1: Create ‘Mega Menu’ custom post type

This is a custom post type that we use to populate the mega menus. It’s not public, which means it doesn’t have a single or archive page and we can only use custom blocks created specifically for building the mega menus layout.

register_post_type(
      'mega-menu',
      array(
        'labels' => array(
          'name' => __('Mega Menus', 'bb-agency'),
          'singular_name' => __('Mega Menu', 'bb-agency'),
          'add_new' => __('Add New Mega Menu', 'bb-agency'),
          'add_new_item' => __('Add New Mega Menu', 'bb-agency'),
          'edit' => __('Edit', 'bb-agency'),
          'edit_item' => __('Edit Mega Menu', 'bb-agency'),
          'new_item' => __('New Mega Menu', 'bb-agency'),
          'view' => __('View Mega Menus', 'bb-agency'),
          'view_item' => __('View Mega Menu', 'bb-agency'),
          'view_items' => __('View Mega Menus', 'bb-agency'),
          'search_items' => __('Search Mega Menus', 'bb-agency'),
          'not_found' => __('No Mega Menus found', 'bb-agency'),
          'not_found_in_trash' => __('No Mega Menus found in Trash', 'bb-agency'),
          'featured_image' => __('Profile Image', 'bb-agency'),
          'set_featured_image' => __('Set Profile Image', 'bb-agency'),
          'remove_featured_image' => __('Remove Profile Image', 'bb-agency'),
          'use_featured_image' => __('Use as Profile Image', 'bb-agency'),
          'archives' => __('Mega Menus Archives', 'bb-agency'),
          'attributes' => __('Mega Menus Attributes', 'bb-agency'),
          'item_published' => __('Mega Menu published.', 'bb-agency'),
        ),
        'description' => 'Mega Menus',
        'public' => true,
        'show_in_rest' => true,
        'hierarchical' => false,
        'has_archive' => false,
        'can_export' => true,
        'exclude_from_search' => true,
        'menu_position' => 20,
        'menu_icon' => 'dashicons-feedback',
        'rewrite' => array('slug' => 'mega-menu', 'with_front' => false),
        'supports' => array(
          'title',
          'editor',
          'custom-fields'
        ),
        'publicly_queryable'  => false // don't create single & archive pages
      )
    );

Step 2: Create custom Gutenberg blocks, to be used for building the mega menus layouts

Let’s take a closer look at some of the custom blocks that we developed specifically for building mega menu layouts, and explore snippets of code from some essential files that we use for registering these Gutenberg blocks.

Mega Menu Columns block

This block is responsible for adding the columns on the mega menus; it only accepts the ‘mega menu column’ as the inner block. By default, it has an equal-width, 3-column layout, but is flexible enough to support multiple layout variations. Of course, these layouts can be updated depending on individual project requirements.

block.json

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "bb/mega-menu-columns",
	"title": "Mega Menu Columns",
	"category": "bb-blocks",
	"version": "1.0.0",
  "styles": [
    {
      "name": "25-75",
      "label": "25/75"
    },
    {
      "name": "75-25",
      "label": "75/25"
    }
  ]
}

index.jsx

/**
 * External dependencies
 */
import classNames from 'classnames';

/**
 * Internal dependencies
 */
import metadata from './block.json';

/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { InnerBlocks, useBlockProps, useInnerBlocksProps } = wp.blockEditor;
const { SVG, Path } = wp.components;

// Create an SVG icon to use as the icon for the block
const icon = <SVG width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
	<Path d="M19 6H6c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-4.1 1.5v10H10v-10h4.9zM5.5 17V8c0-.3.2-.5.5-.5h2.5v10H6c-.3 0-.5-.2-.5-.5zm14 0c0 .3-.2.5-.5.5h-2.6v-10H19c.3 0 .5.2.5.5v9z"></Path>
</SVG>

registerBlockType(metadata, {
	icon,
	edit: (props) => {
		const blockProps = useBlockProps({
			className: classNames(
				'mega-menu-columns',
				'block-edit__mega-menu-columns',
			),
		});

		// Only allow mega menu column blocks
		const ALLOWED_BLOCKS = ['bb/mega-menu-column'];

		const innerBlocksProps = useInnerBlocksProps(blockProps, {
			allowedBlocks: ALLOWED_BLOCKS,
			orientation: 'horizontal',
			renderAppender: InnerBlocks.ButtonBlockAppender
		});

		return <div {...innerBlocksProps}></div>
	},
	save: props => <InnerBlocks.Content />
});

index.php

<?php

function render_block_mega_menu_columns($attributes, $content)
{

	$style = '';
	$class = 'mega-menu-columns';

	$wrapper_attributes = get_block_wrapper_attributes(['class' => $class, 'style' => $style]);

	ob_start();
?>

	<div <?php echo $wrapper_attributes; ?>>
		<?php echo $content; ?>
	</div>

<?php
	return ob_get_clean();
}

register_block_type(
	__DIR__,
	array(
		'render_callback' => 'render_block_mega_menu_columns',
	)
);

Mega Menu Column block

This block is the inner block used inside the ‘mega menu columns’ parent block. And we use it as a wrapper for our column content.

block.json

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 2,
  "name": "bb/mega-menu-column",
  "title": "Mega Menu Column",
  "category": "bb-blocks",
  "version": "1.0.0",
  "parent": [
    "bb/mega-menu-columns"
  ],
  "attributes": {
    "bbVariation": {
      "type": "string",
      "default": ""
    },
    "verticalAlignment": {
      "type": "string",
      "default": ""
    },
    "isFlexDirectionColumn": {
      "type": "boolean",
      "default": false
    },
    "minHeight": {
      "type": "number"
    },
    "verticalGap": {
      "type": "number",
      "default": 4
    }
  },
  "supports": {
    "color": {
      "background": true,
      "color": true
    },
    "spacing": {
      "padding": true
    }
  },
  "variations": [
    {
      "name": "mega-menu-column",
      "title": "Mega Menu Column",
      "isDefault": true,
      "scope": [
        "transform"
      ],
      "attributes": {
        "bbVariation": "",
        "className": "",
        "isFlexDirectionColumn": false
      },
      "isActive": [
        "bbVariation"
      ]
    },
    {
      "name": "mega-menu-column--stack",
      "title": "Mega Menu Column - Stack",
      "description": "Arrange blocks vertically.",
      "scope": [
        "transform"
      ],
      "attributes": {
        "bbVariation": "mega-menu-column--stack",
        "isFlexDirectionColumn": true
      },
      "isActive": [
        "bbVariation"
      ]
    }
  ]
}

index.jsx

/**
 * External dependencies
 */
import classNames from 'classnames';

/**
 * Internal dependencies
 */
import metadata from './block.json';


/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { InnerBlocks, InspectorControls, useBlockProps, useInnerBlocksProps, BlockControls, BlockVerticalAlignmentToolbar, JustifyContentControl, store: blockEditorStore } = wp.blockEditor;
const { PanelBody, PanelRow, TextControl, ToggleControl, SelectControl, Button, RangeControl, SVG, Path, RadioControl, ColorPalette } = wp.components;
const { __ } = wp.i18n;
const { useSelect } = wp.data;

// Create an SVG icon to use as the icon for the block
const icon = <SVG width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
	<Path d="M19 6H6c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2h13c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zM6 17.5c-.3 0-.5-.2-.5-.5V8c0-.3.2-.5.5-.5h3v10H6zm13.5-.5c0 .3-.2.5-.5.5h-3v-10h3c.3 0 .5.2.5.5v9z"></Path>
</SVG>

registerBlockType(metadata, {
	icon,
	edit: (props) => {
		const { attributes, setAttributes, className, clientId } = props;

		const {
			verticalAlignment, isFlexDirectionColumn, minHeight, verticalGap
		} = attributes;

		const { innerBlocks } = useSelect((select) => {
			const { getBlock } = select(blockEditorStore);
			const block = getBlock(clientId);
			return {
				innerBlocks: block.innerBlocks
			};
		}, [clientId]);

		// Check if the inner blocks contain another mega menu columns block
		const hasInnerMegaMenuColumns = (innerBlocks.findIndex((block) => block.name === "bb/mega-menu-columns") !== -1);

		const blockProps = useBlockProps({
			className: classNames(
				'mega-menu-column',
				{ 'has-inner-mega-menu-columns': hasInnerMegaMenuColumns },
				{ [`align-self--${verticalAlignment}`]: verticalAlignment },
				{ 'flex-direction-column': isFlexDirectionColumn },
				{ 'has-min-height-set': minHeight },
			),
			style: {
				...(minHeight && { '--min-height': `${minHeight}px` }),
				...(verticalGap && isFlexDirectionColumn && { 'gap': `${verticalGap}px` }),
			},
		});

		const innerBlocksProps = useInnerBlocksProps(blockProps, {
			renderAppender: (innerBlocks.length) ? undefined : InnerBlocks.ButtonBlockAppender
		});

		return (
			<>
				<div {...innerBlocksProps}></div>

				<BlockControls group="block">
					<BlockVerticalAlignmentToolbar
						onChange={(verticalAlignment) => { setAttributes({ verticalAlignment }); }}
						value={verticalAlignment}
					/>
				</BlockControls>

				<InspectorControls>
					<PanelBody title="General Settings" initialOpen={true} >
						<RangeControl
							label="Minimum Height (px)"
							value={minHeight}
							onChange={(minHeight) => setAttributes({ minHeight })}
							min={140}
							step={2}
							max={600}
							allowReset={true}
						/>
					</PanelBody>
				</InspectorControls>
			</>
		)
	},
	save: props => <InnerBlocks.Content />

});

index.php

<?php

function render_block_mega_menu_column($attributes, $content, $block)
{


	$style = '';
	$class = 'mega-menu-column';

	$verticalAlignment = isset($attributes['verticalAlignment']) ? $attributes['verticalAlignment'] : '';

	$isFlexDirectionColumn = isset($attributes['isFlexDirectionColumn']) ? $attributes['isFlexDirectionColumn'] : true;
	$minHeight = isset($attributes['minHeight']) ? $attributes['minHeight'] : 0;

	$verticalGap = isset($attributes['verticalGap']) ? $attributes['verticalGap'] : 0;

	$innerBlocks = $block->parsed_block["innerBlocks"];

	// Check if column contains another set of mega menu columns
	$hasInnerMegaMenuColumns = array_search('bb/mega-menu-columns', array_column($innerBlocks, 'blockName'));

	// Check if column countains navigation link card
	$hasInnerNavigationLinkCard = array_search('bb/navigation-link-card', array_column($innerBlocks, 'blockName'));

	if ($hasInnerMegaMenuColumns !== false) {
		$class .= " has-inner-mega-menu-columns";
	}

	if ($hasInnerNavigationLinkCard !== false) {
		$class .= " has-inner-navigation-link-card";
	}

	if ($verticalAlignment) {
		$class .= " align-self--{$verticalAlignment}";
	}


	if ($isFlexDirectionColumn) {
		$class .= " flex-direction-column";
	}

	// Min Height Setting
	if ($minHeight) {
		$class .= ' has-min-height-set';
		$style .= "--min-height:{$minHeight}px;";
	}

	// Vertical Gap
	if($verticalGap && $isFlexDirectionColumn) {
		$style .= "gap:{$verticalGap}px;";
	}

	$wrapper_attributes = get_block_wrapper_attributes(['class' => $class, 'style' => $style]);

	ob_start();
?>

	<div <?php echo $wrapper_attributes; ?>>
		<?php echo $content; ?>
	</div>

<?php
	return ob_get_clean();
}


register_block_type(
	__DIR__,
	array(
		'render_callback' => 'render_block_mega_menu_column',
	)
);

This block gives us the ability to add link cards on our mega menus, it outputs a card that will be linked to a destination specified by the user. It supports attributes like title, description, background image, and provides customization options for both its default and hover state colors.

block.json

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "bb/navigation-link-card",
	"title": "Navigation Link Card",
	"icon": "button",
	"category": "bb-blocks",
	"version": "1.0.0",
	"attributes": {
		"title": {
			"type": "string"
		},
		"link": {
			"type": "string"
		},
		"opensInNewTab": {
			"type": "boolean",
			"default": false
		},
		"description": {
			"type": "string"
		},
		"titleColor": {
			"type": "string",
			"default": "#000000"
		},
		"backgroundColor": {
			"type": "string",
			"default": "#FFFFFF"
		},
		"contentColor": {
			"type": "string",
			"default": "#767676"
		},
		"titleColorHover": {
			"type": "string",
			"default": "#000000"
		},
		"backgroundColorHover": {
			"type": "string"
		},
		"contentColorHover": {
			"type": "string",
			"default": "#000000"
		},
		"bgImageId": {
			"type": "number",
			"default": 0
		},
		"bgImageUrl": {
			"type": "string"
		}
	},
	"supports": {
		"spacing": {
			"margin": [
				"top",
				"bottom"
			]
		},
		"html": false
	}
}

index.jsx

/**
 * External dependencies
 */
import classNames from 'classnames';

/**
 * Internal dependencies
 */
import metadata from './block.json';

/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { InnerBlocks, InspectorControls, useBlockProps, RichText, BlockControls, ColorPaletteControl, useInnerBlocksProps, MediaReplaceFlow, PanelColorSettings } = wp.blockEditor;
const { PanelBody, PanelRow, TextControl, TextareaControl, ToggleControl, SelectControl, ButtonGroup, Button, RangeControl, SVG, Path, ToolbarGroup, ToolbarButton, Popover, ColorPalette, FontSizePicker } = wp.components;
const { useState } = wp.element;
const { useSelect } = wp.data;
const { __ } = wp.i18n;

const { LinkControl: __stableLinkControl, __experimentalLinkControl } = wp.blockEditor;
const LinkControl = __stableLinkControl
	? __stableLinkControl
	: __experimentalLinkControl;

registerBlockType(metadata, {
	edit: (props) => {
		const { attributes, setAttributes, className, context } = props;

		const {
			title, link, opensInNewTab,
			description,
			titleColor, backgroundColor, contentColor,
			titleColorHover, backgroundColorHover, contentColorHover,
			bgImageId, bgImageUrl,
		} = attributes;

		const [popoverAnchor, setPopoverAnchor] = useState();
		const [isPopoverVisible, setIsPopoverVisible] = useState(false);
		const togglePopoverVisible = () => {
			setIsPopoverVisible((state) => !state);
		};

		const blockProps = useBlockProps({
			className: classNames(
				'navigation-link-card',
				'block-edit__navigation-link-card',
				{ 'has-bg-image': bgImageId },
			),
			style: {
				...(titleColor && { '--title-color': titleColor }),
				...(backgroundColor && { '--bg-color': backgroundColor }),
				...(contentColor && { '--content-color': contentColor }),
				...(titleColorHover && { '--title-color-hover': titleColorHover }),
				...(backgroundColorHover && { '--bg-color-hover': backgroundColorHover }),
				...(contentColorHover && { '--content-color-hover': contentColorHover }),
			}
		});

		const ALLOWED_BLOCKS = ['bb/simple-heading', 'bb/simple-paragraph', 'bb/simple-image', 'core/list'];

		const innerBlocksProps = useInnerBlocksProps({ className: "navigation-link-card__content" }, {
			allowedBlocks: ALLOWED_BLOCKS,
		});

		return (
			<>
				<div {...blockProps}>
					<div className="navigation-link-card__header">
						<div className={`navigation-link-card__title`}>
							<RichText
								tagName="div" // The tag here is the element output and editable in the admin
								value={title}
								onChange={(title) => { setAttributes({ title }) }}
								placeholder="Link card title"
								allowedFormats={['core/text-color']}
							/>
							<i className="icon-chevron-right"></i>
						</div>
						<RichText
							tagName="div" // The tag here is the element output and editable in the admin
							value={description}
							onChange={(description) => { setAttributes({ description }) }}
							placeholder="Link card description (optional)"
							className="navigation-link-card__description"
							allowedFormats={[]}
						/>
					</div>

					<div {...innerBlocksProps}></div>

					{bgImageUrl && <img src={bgImageUrl} className="navigation-link-card__bg-image" />}
				</div>

				<BlockControls>
					<ToolbarGroup>
						<ToolbarButton
							icon='admin-links'
							label="Add/Edit Link"
							onClick={() => setIsPopoverVisible(true)}
							isPressed={link}
							ref={setPopoverAnchor}
						/>
						{isPopoverVisible &&
							<Popover position="bottom center" anchor={popoverAnchor} onFocusOutside={() => { setIsPopoverVisible(false) }} >
								<div style={{ padding: '10px' }}>
									<LinkControl
										value={{
											url: link,
											title: title,
											opensInNewTab
										}}
										onChange={(nextValue) => {
											setAttributes({
												link: nextValue.url,
												title: nextValue.title,
											});

											if (opensInNewTab !== nextValue.opensInNewTab) {
												setAttributes({ opensInNewTab: !opensInNewTab });
											}
										}}
										onRemove={() => {
											setAttributes({
												link: undefined,
												title: undefined,
												opensInNewTab: undefined,
											});
										}}
										settings={[
											{
												id: 'opensInNewTab',
												title: 'Open in new tab',
											},
										]}
									/>
								</div>
								<Button variant="primary" onClick={() => setIsPopoverVisible(false)} style={{ width: '100%' }}>Close</Button>
							</Popover>
						}
					</ToolbarGroup>

					<ToolbarGroup>
						<MediaReplaceFlow
							mediaURL={bgImageUrl}
							mediaId={bgImageId}
							allowedTypes={['image']}
							onSelect={(media) => {
								setAttributes({ bgImageId: media.id });
								setAttributes({ bgImageUrl: media.url });
							}}
							name={!bgImageUrl ? 'Add Background Image' : 'Replace Background Image'}
						/>
					</ToolbarGroup>
					<ToolbarGroup className="toolbar-remove-image-group">
						{bgImageUrl && (
							<ToolbarButton
								isDestructive
								icon="trash"
								label="Remove Background Image"
								className="toolbar-remove-image-btn"
								onClick={() => setAttributes({ bgImageId: null, bgImageUrl: null })}
							/>
						)}
					</ToolbarGroup>
				</BlockControls>

				<InspectorControls>
					{!bgImageUrl && (
						<>
							<PanelBody title="Colors" initialOpen={false} >
								<ColorPaletteControl
									label="Title Color"
									value={titleColor}
									onChange={(titleColor) => setAttributes({ titleColor })}
									disableCustomColors={false}
								/>
								<ColorPaletteControl
									label="Content Color"
									value={contentColor}
									onChange={(contentColor) => setAttributes({ contentColor })}
									disableCustomColors={false}
								/>
								<hr />
								<ColorPaletteControl
									label="Background Color"
									value={backgroundColor}
									onChange={(backgroundColor) => setAttributes({ backgroundColor })}
									disableCustomColors={false}
								/>
							</PanelBody>

							<PanelBody title="Hover Colors" initialOpen={false} >
								<ColorPaletteControl
									label="Title Color"
									value={titleColorHover}
									onChange={(titleColorHover) => setAttributes({ titleColorHover })}
									disableCustomColors={false}
								/>
								<ColorPaletteControl
									label="Content Color"
									value={contentColorHover}
									onChange={(contentColorHover) => setAttributes({ contentColorHover })}
									disableCustomColors={false}
								/>

								<hr />
								<ColorPaletteControl
									label="Background Color"
									value={backgroundColorHover}
									onChange={(backgroundColorHover) => setAttributes({ backgroundColorHover })}
									disableCustomColors={false}
								/>
							</PanelBody>
						</>
					)}
				</InspectorControls>
			</>
		)
	},
	save: props => <InnerBlocks.Content />
});

index.php

<?php

function render_block_navigation_link_card($attributes, $content, $block)
{

	$wrapper_class = 'navigation-link-card-wrapper';
	$wrapper_style = '';


	$style = '';
	$class = 'navigation-link-card';

	$titleClass = 'navigation-link-card__title';

	$title = isset($attributes['title']) ? $attributes['title'] : '';
	$link = isset($attributes['link']) ? $attributes['link'] : '';
	$opensInNewTab = isset($attributes['opensInNewTab']) ? $attributes['opensInNewTab'] : false;

	$description = isset($attributes['description']) ? $attributes['description'] : '';

	$bgImageId = isset($attributes['bgImageId']) ? $attributes['bgImageId'] : 0;
	$bgImageUrl = isset($attributes['bgImageUrl']) ? $attributes['bgImageUrl'] : '';

	$titleColor = isset($attributes['titleColor']) ? $attributes['titleColor'] : '';
	$contentColor = isset($attributes['contentColor']) ? $attributes['contentColor'] : '';
	$backgroundColor = isset($attributes['backgroundColor']) ? $attributes['backgroundColor'] : '';

	$titleColorHover = isset($attributes['titleColorHover']) ? $attributes['titleColorHover'] : $titleColor;
	$contentColorHover = isset($attributes['contentColorHover']) ? $attributes['contentColorHover'] : $contentColor;
	$backgroundColorHover = isset($attributes['backgroundColorHover']) ? $attributes['backgroundColorHover'] : $backgroundColor;

	if (!$bgImageId) {
		// Colors
		if ($titleColor) {
			$class .= ' has-title-color-set';
			$wrapper_style .= "--title-color:{$titleColor};";
		}
		if ($contentColor) {
			$class .= ' has-content-color-set';
			$wrapper_style .= "--content-color:{$contentColor};";
		}
		if ($backgroundColor) {
			$class .= ' has-bg-color-set';
			$wrapper_style .= "--bg-color:{$backgroundColor};";
		}

		// Hover Colors
		if ($titleColorHover) {
			$wrapper_style .= "--title-color-hover:{$titleColorHover};";
		}
		if ($contentColorHover) {
			$wrapper_style .= "--content-color-hover:{$contentColorHover};";
		}
		if ($backgroundColorHover) {
			$wrapper_style .= "--bg-color-hover:{$backgroundColorHover};";
		}
	} else {
		$class .= ' has-bg-image';
	}

	$wrapper_attributes = get_block_wrapper_attributes(['class' => $class, 'style' => $style]);

	if (!$title) return;


	$link_start = ($link) ? '<a href="' . $link . '"' . ($opensInNewTab ? 'target="_blank"' : '') . $wrapper_attributes . '>' :  '<div ' . $wrapper_attributes . '>';
	$link_end = ($link) ? '</a>' : '</div>';

	ob_start();
?>

	<div class="<?php echo $wrapper_class; ?>" style="<?php echo $wrapper_style; ?>">
		<?php echo $link_start; ?>

		<div class="navigation-link-card__header">
			<div class="<?php echo $titleClass; ?>">
				<span><?php echo $title; ?></span>
				<i class="icon-chevron-right"></i>
			</div>

			<?php if ($description) : ?>
				<span class="navigation-link-card__description"><?php echo $description; ?></span>
			<?php endif; ?>
		</div>

		<div class="navigation-link-card__content">
			<?php echo $content; ?>
		</div>

		<?php if ($bgImageId) : ?>
			<?php echo wp_get_attachment_image($bgImageId, 'medium_large', false, array('class' => 'navigation-link-card__bg-image')); ?>
		<?php endif; ?>

		<?php echo $link_end; ?>
	</div>


<?php
	return ob_get_clean();
}


register_block_type(
	__DIR__,
	array(
		'render_callback' => 'render_block_navigation_link_card',
	)
);

Some other blocks that we use inside the mega menus editor include

  • Navigation List: Outputs a list which accepts navigation links as inner block
  • Navigation Link: This is the block that we use for adding links to our navigation list

Now let’s take a closer look at how we can add a custom field to our menu items, where it will be used to connect the menu items with the mega menus.

<?php 

/**
 * Add custom fields to the menu item in the menu editor.
 *
 * @param int    $item_id  The ID of the menu item.
 * @param object $item     The menu item object.
 * @param int    $depth    The depth of the menu item.
 * @param array  $args     An array of arguments.
 */
function bb_add_menu_items_custom_fields($item_id, $item, $depth, $args)
{
	$selectedMegaMenu = get_post_meta($item_id, '_menu-item-mega-menu-select', true);
?>

	<!-- Mega menu select -->
	<p class="menu-item__custom-field field--mega-menu-select description description-wide">
		<label>
			<?php _e("Mega Menu Select (Main menu only)", 'bb-agency'); ?> <br>
			<select class="widefat" name="menu-item-mega-menu-select[<?php echo $item_id; ?>]" id="menu-item-mega-menu-select-<?php echo $item_id; ?>">
				<option value="" <?php selected($selectedMegaMenu, '') ?>> None </option>
				<?php
				$args = array(
					'post_type' => 'mega-menu',
					'post_status' => 'publish',
					'posts_per_page' => -1,
				);
				$the_query = new WP_Query($args); ?>
				<?php if ($the_query->have_posts()) : ?>
					<?php while ($the_query->have_posts()) : $the_query->the_post(); ?>
						<option value="<?php echo get_the_ID() ?>" <?php selected($selectedMegaMenu, get_the_ID()) ?>>
							<?php echo get_the_title() ?>
						</option>
					<?php endwhile; ?>
					<?php wp_reset_postdata(); ?>
				<?php endif; ?>
			</select>
		</label>
	</p>

<?php
}
add_action('wp_nav_menu_item_custom_fields', 'bb_add_menu_items_custom_fields', 10, 4);


/**
 * Save custom fields for menu items when a menu is updated.
 *
 * @param int   $menu_id          The ID of the updated menu.
 * @param int   $menu_item_db_id  The ID of the menu item being updated.
 * @param array $args             An array of arguments.
 */
function bb_update_menu_items_custom_fields($menu_id, $menu_item_db_id, $args)
{

	$fields = array('mega-menu-select');

	foreach ($fields as $key) {
		$value = (isset($_POST['menu-item-' . $key][$menu_item_db_id])) ? $_POST['menu-item-' . $key][$menu_item_db_id] : '';
		update_post_meta($menu_item_db_id, '_menu-item-' . $key, $value);
	}
}
add_action('wp_update_nav_menu_item', 'bb_update_menu_items_custom_fields', 10, 3);

Step 4: Display mega menu content using custom WordPress Nav Menu Walker

In this step, we will explore the custom WordPress Nav Menu Walker class we’ve developed and how we utilize it to display our mega menus within the main menu.

Custom WordPress_Nav_Menu class

<?php 
class BB_Agency_Walker extends Walker_Nav_Menu
{

	/**
	 * Handle the starting of a navigation menu item.
	 *
	 * @param string    &$output            The menu item's HTML output.
	 * @param WP_Post   $item               The current menu item.
	 * @param int       $depth              Depth of the current menu item.
	 * @param stdClass  $args               An object containing menu arguments.
	 * @param int       $current_object_id  The ID of the current object.
	 */
	function start_el(&$output, $item, $depth = 0, $args = null, $current_object_id = 0)
	{

		$object = $item->object;
		$type = $item->type;
		$title = $item->title;
		$permalink = $item->url;
		$id = $item->ID;
		$target = $item->target;

		$selectedMegaMenu = get_post_meta($id, '_menu-item-mega-menu-select', true);

		// Setup menu item classes
		$classes = apply_filters('nav_menu_css_class', array_filter($item->classes), $item, $args, $depth);

		$titleIcon = '';

		if ($selectedMegaMenu && $depth === 0) {
			$classes[] = 'menu-item-has-mega-menu';
			$titleIcon = '<i class="icon-chevron-down"></i>';
		}

		// Setup default elements
		$defaultStart = '<li class="' . implode(" ", $classes) . '">';

		$defaultTitle = '<a href="' . $permalink . '"' . ($target ? 'target="_blank"' : '') . '>' . $title . $titleIcon . '</a>';

		// Default output
		$defaultOutput =  $defaultTitle;

		$output .= $defaultStart;

		// Return output
		$output .= apply_filters('walker_nav_menu_start_el', $defaultOutput, $item, $depth, $args);
	}


	/**
	 * Handle the ending of a navigation menu item.
	 *
	 * @param string  &$output The menu item's HTML output.
	 * @param WP_Post $item    The current menu item.
	 * @param int     $depth   Depth of the current menu item.
	 * @param stdClass $args    An object containing menu arguments.
	 */
	function end_el(&$output, $item, $depth = 0, $args = null)
	{

		// Get the selected mega menu for the current item
		$selectedMegaMenu = get_post_meta($item->ID, '_menu-item-mega-menu-select', true);
		$selectedMegaMenuContent = apply_filters('the_content', get_post_field('post_content', $selectedMegaMenu));

		// Get item title
		$title = $item->title;

		if ($selectedMegaMenu) {
			// Check if animation is enabled for the mega menu
			$enableAnimation = get_post_meta($selectedMegaMenu, '_enable_animation', true);

			$class = 'mega-menu-wrapper';

			if (!$enableAnimation) {
				$class .= ' no-animation';
			}

			// Generate the HTML for the mega menu content
			$output .= '<div class="' . $class . '">';
			$output .= '<div class="mega-menu__content-wrapper">';
			$output .= '<div class="container">';
			$output .= '<div class="mega-menu__content js-mega-menu__content">';
			$output .=  '<button class="mega-menu-back-btn"><i class="icon-chevron-left"></i>' .  $title . '</button>';
			$output .= $selectedMegaMenuContent;
			$output .= '</div>';
			$output .= '</div>';
			$output .= '</div>';
			$output .= '</div>';
		}

		$output .= '</li>'; // close li
	}
}

Output the menu on website’s header

<?php
	wp_nav_menu(array(
		'theme_location' => 'header-menu',
		'container' => 'nav',
		'container_class' => 'header-main-menu-wrapper',
		'walker' => new BB_Agency_Walker(),
		'depth' => 1
	));
?>

That’s about it when it comes to the functionality and the logic for building the mega menus. Now it’s just a matter of some custom CSS & Javascript code to handle the appearance & functionality of our mega menus.

Wrapping Up

In this blog post we’ve explored our tailored approach to WordPress mega menus, and how we integrated classic WordPress menus with the modern Gutenberg editor to create a streamlined and user-friendly way of managing mega menus. This custom approach showcases the adaptability and limitless capabilities of using the Gutenberg editor to create unique and dynamic website experiences.

Share this article:

LinkedIn Twitter Facebook

Author