Navigation + Toggle/Hamburger Menu for Hugo

Here’s how I got a Hamburger/Toggle menu to work on my Hugo site by borrowing aspects of WP’s _s starter theme.

First copy and paste the JS code below and save the file as navigation.js in the static/js folder of Hugo and then add it to every layout where you want the menu appears, just before the closing </body> tag (<script src="{{ .Site.BaseURL }}/js/scripts.min.js" defer></script>).

( function() {
	const siteNavigation = document.getElementById( 'site-navigation' );

	// Return early if the navigation don't exist.
	if ( ! siteNavigation ) {
		return;
	}

	const button = siteNavigation.getElementsByTagName( 'button' )[ 0 ];

	// Return early if the button don't exist.
	if ( 'undefined' === typeof button ) {
		return;
	}

	const menu = siteNavigation.getElementsByTagName( 'ul' )[ 0 ];

	// Hide menu toggle button if menu is empty and return early.
	if ( 'undefined' === typeof menu ) {
		button.style.display = 'none';
		return;
	}

	if ( ! menu.classList.contains( 'nav-menu' ) ) {
		menu.classList.add( 'nav-menu' );
	}

	// Toggle the .toggled class and the aria-expanded value each time the button is clicked.
	button.addEventListener( 'click', function() {
		siteNavigation.classList.toggle( 'toggled' );

		if ( button.getAttribute( 'aria-expanded' ) === 'true' ) {
			button.setAttribute( 'aria-expanded', 'false' );
		} else {
			button.setAttribute( 'aria-expanded', 'true' );
		}
	} );

	// Remove the .toggled class and set aria-expanded to false when the user clicks outside the navigation.
	document.addEventListener( 'click', function( event ) {
		const isClickInside = siteNavigation.contains( event.target );

		if ( ! isClickInside ) {
			siteNavigation.classList.remove( 'toggled' );
			button.setAttribute( 'aria-expanded', 'false' );
		}
	} );

	// Get all the link elements within the menu.
	const links = menu.getElementsByTagName( 'a' );

	// Get all the link elements with children within the menu.
	const linksWithChildren = menu.querySelectorAll( '.menu-item-has-children > a, .page_item_has_children > a' );

	// Toggle focus each time a menu link is focused or blurred.
	for ( const link of links ) {
		link.addEventListener( 'focus', toggleFocus, true );
		link.addEventListener( 'blur', toggleFocus, true );
	}

	// Toggle focus each time a menu link with children receive a touch event.
	for ( const link of linksWithChildren ) {
		link.addEventListener( 'touchstart', toggleFocus, false );
	}

	/**
	 * Sets or removes .focus class on an element.
	 */
	function toggleFocus() {
		if ( event.type === 'focus' || event.type === 'blur' ) {
			let self = this;
			// Move up through the ancestors of the current link until we hit .nav-menu.
			while ( ! self.classList.contains( 'nav-menu' ) ) {
				// On li elements toggle the class .focus.
				if ( 'li' === self.tagName.toLowerCase() ) {
					self.classList.toggle( 'focus' );
				}
				self = self.parentNode;
			}
		}

		if ( event.type === 'touchstart' ) {
			const menuItem = this.parentNode;
			event.preventDefault();
			for ( const link of menuItem.parentNode.children ) {
				if ( menuItem !== link ) {
					link.classList.remove( 'focus' );
				}
			}
			menuItem.classList.toggle( 'focus' );
		}
	}
}() );

Second, ensure you have created a menu. Here is an example of a menu in the config.toml file

[menu]
[[menu.main]]
name = 'Home'
pageref = '/'
weight = 1

[[menu.main]]
name = 'About'
pageref = '/about'
weight = 2

[[menu.main]]
name = 'Contact'
pageref = '/contact'
weight = 3

[[menu.main]]
name = 'Category A'
pageref = '/categories/category-a'

[[menu.main]]
name = 'Category B'
pageref = '/categories/category-b'

[[menu.main]]
name = 'Category C'
pageref = '/categories/category-c'

And you can include it in your header file like this

<div class="navigation-wrapper"> 
   <nav id="site-navigation" class="main-navigation">
  <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false"><span class="site-icon"><svg viewBox="0 0 512 512" aria-hidden="true" width="1em" height="1em"><path d="M0 96c0-13.255 10.745-24 24-24h464c13.255 0 24 10.745 24 24s-10.745 24-24 24H24c-13.255 0-24-10.745-24-24zm0 160c0-13.255 10.745-24 24-24h464c13.255 0 24 10.745 24 24s-10.745 24-24 24H24c-13.255 0-24-10.745-24-24zm0 160c0-13.255 10.745-24 24-24h464c13.255 0 24 10.745 24 24s-10.745 24-24 24H24c-13.255 0-24-10.745-24-24z"></path></svg></span><span class="mobile-menu">Menu</span></button>
<ul class="main-menu">
  {{ $currentPage := . }}
  {{ range site.Menus.main }}
    {{ if $currentPage.IsMenuCurrent "main" . }}
      <li class="active">
        <a href="{{ .URL }}">{{ .Name }}</a>
      </li>
    {{ else if and (not .Page.IsHome) (in .Page.Pages $currentPage) }}
      <li class="active">
        <a href="{{ .URL }}">{{ .Name }} </a>
      </li>
    {{ else }}
      <li>
        <a href="{{ .URL }}">{{ .Name }}</a>
      </li>
    {{ end }}
  {{ end }}
</ul>
</div> 
 </div>

Then add this CSS either inline the header or in a separate CSS file that you call in the header.

.main-navigation {
	display: block;
	width: 100%;
}

.main-navigation ul {
	display: none;
	list-style: none;
	margin: 0;
	padding-left: 0;
}

.main-navigation ul ul {
	box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2);
	float: left;
	position: absolute;
	top: 100%;
	left: -999em;
	z-index: 99999;
}

.main-navigation ul ul ul {
	left: -999em;
	top: 0;
}

.main-navigation ul ul li:hover > ul,
.main-navigation ul ul li.focus > ul {
	display: block;
	left: auto;
}

.main-navigation ul ul a {
	width: 200px;
}

.main-navigation ul li:hover > ul,
.main-navigation ul li.focus > ul {
	left: auto;
}

.main-navigation li {
	position: relative;
}

.main-navigation a {
	display: block;
	text-decoration: none;
}

/* Small menu. */
.menu-toggle,
.main-navigation.toggled ul {
	display: block;
}

@media screen and (min-width: 769px) {

	.menu-toggle {
		display: none;
	}

	.main-navigation ul {
		display: flex;
	}
}

This code will show your navigation menu unstyled, and the toggle menu will ‘toggle’ but it will be unstyled too. It is up to you now to style it as you wish. Change @media screen and (min-width: 769px) to your desired breakpoint. Cheers!

NB: I haven’t tested if the toggle works with submenus.