How to add a resizable desktop navbar in Bricks

How to add a resizable desktop navbar in Bricks

This tutorial will show you how to add a “More” menu item when the navbar overflows on desktop.

Table of Contents

Introduction

The UI/UX literature recommends having as less menu items as possible. But if you’re a web professional, you certainly know that some client insists to have a bunch of items inside their navbar and it ends up being a nightmare to style on different breakpoints in order to avoid the overflow.

Well, there is an alternative. Instead of using tons of media queries, you could just integrate this small script. As soon as your menu overflows on resize, the script will automatically create a “More” item at the end of your navbar, and list the overflown items as sub-menu items. Let’s see how to implement it.

The DOM Structure

Follow the following DOM tree:

Make sure to add the menu-wrapper, menu-inner and menu-crop classes to the correct elements as shown above.

The menu-wrapper element needs to have a width of 100%. If you want your navbar to be boxed, set the max-width as well.

The menu-inner block needs to use flexbox, and use flex-direction: row:

JavaScript

Copy/Paste the following JavaScript code where it needs to be applied. As an example, if you’re menu is global and runs on all pages, you could paste the code in Bricks Settings -> Custom Code tab -> Body (footer) scripts.

window.addEventListener('load', () => {

   // Variables
   const wrapper = document.querySelector('.menu-wrapper');
   if (!wrapper) {
      return console.log('Resizable Desktop Menu - There is no menu-wrapper class on this page. Please double check you correctly assigned the classes to each element');
   }
   const inner = wrapper.querySelector('.menu-inner');
   if (!inner) {
      return console.log('Resizable Desktop Menu - There is no menu-inner class on this page. Please double check you correctly assigned the classes to each element');
   }
   const menu = inner.querySelector('.menu-crop');
   if (!menu) {
      return console.log('Resizable Desktop Menu - There is no menu-crop class on this page. Please double check you correctly assigned the classes to each element');
   }
   const menuUl = menu.querySelector('ul.bricks-nav-menu');
   const menuItems = menu.querySelectorAll("ul.bricks-nav-menu > li.menu-item").length;
   let hiddenItems = [];
   let moreItem;
   let moreIsVisible = false;

   // debounce so calculation doesn't happen every millisecond when resizing
   const debounce = (fn, threshold) => {
      var timeout;
      threshold = threshold || 200;
      return function debounced() {
         clearTimeout(timeout);
         var args = arguments;
         var _this = this;

         function delayed() {
            fn.apply(_this, args);
         }
         timeout = setTimeout(delayed, threshold);
      };
   };

   // Check if overflow
   const isOverflown = (element) => {
      return element.scrollWidth > element.clientWidth;
   }

   // Create elements function
   const createElement = (el, options, append) => {
      let element = document.createElement(el);

      if (!options) return element;
      let entries = Object.entries(options)
      let data = entries.map(([key, val] = entry) => {
         element.setAttribute(key, val);
      });

      if (!append) return element
      return append.appendChild(element);
   }

   // Create More Item
   const createMoreMenu = (menuUl) => {

      // Create Elemets
      const tag = createElement('li', {
         'id': 'moreItem',
         'class': 'bricks-menu-item'
      }, menuUl);
      const active = createElement('a', {}, tag);
      const text = document.createTextNode("More");
      active.appendChild(text);
      const qty = createElement('span', {
         'id': 'moreQty'
      }, active);
      const ulTag = createElement('ul', {
         'id': 'moreSubMenu',
         'role': 'menu',
         'class': 'sub-menu'
      }, tag);

      moreIsVisible = true;
   }

   // Remove Item
   const removeItem = () => {
      // Stop the function is NOT overflown
      if (!isOverflown(inner) || hiddenItems.length == menuItems) return;

      // Declare Variables
      let menuLi = menu.querySelectorAll("ul.bricks-nav-menu > li.menu-item");
      let lastItem = menuLi[menuLi.length - 1];

      // Add node to hidden array
      hiddenItems.push(lastItem);

      // Remove node from the DOM
      if (lastItem) menuUl.removeChild(lastItem);

      // Insert the More Button
      if (!moreIsVisible) createMoreMenu(menuUl);

      // Append the item to the sub menu
      let subMenu = menu.querySelector('#moreSubMenu');
      if (subMenu) subMenu.insertBefore(lastItem, subMenu.firstChild);

      // Update the Qty Number
      let subMenuQty = menu.querySelector('#moreQty');
      subMenuQty.innerHTML = hiddenItems.length;

      // Keep removing items if the inner container is overflown
      removeItem();
   }

   // Add Item
   const addItem = () => {
      if (hiddenItems.length < 1) return;

      // Remove the More Item
      if (moreIsVisible) {
         let moreItem = menuUl.querySelector("#moreItem");
         moreItem.remove();
         moreIsVisible = false;
      }

      // Readd all hidden items as visible
      if (hiddenItems.length > 0) {
         hiddenItems.slice().reverse().forEach(item => {
            menuUl.insertBefore(item, moreItem);
         });

         // reset the hidden array
         hiddenItems = [];
      }

      // Remove item is the inner container is overflown
      if (isOverflown(inner)) removeItem();
   }

   // Init
   if (isOverflown(inner)) removeItem();

   // Resize
   window.addEventListener('resize', debounce(() => {
      isOverflown(inner) ? removeItem() : addItem();
   }, 0))

})

CSS

Paste the following CSS snippet where the script is running. As an example, if you’re menu is global and runs on all pages, you could paste the code in Bricks Settings -> Custom Code tab -> Custom CSS.

.menu-inner > * {
   white-space: nowrap;
}

#moreItem a {
   display: flex;
   align-items: flex-start;
   flex-wrap: nowrap;
   flex-direction: row;
}

span#moreQty {
   margin-left: 0.3rem;
   background-color: var(--bricks-color-primary, blue);
   padding: 0.3rem 0.45rem;
   font-size: 9px;
   color: #fff;
   border-radius: 50%;
   vertical-align: super;
   font-weight: 800;
   width: 15px;
   height: 15px;
   display: flex;
   align-items: center;
   justify-content: center;
}

Resources

debounce function

https://www.freecodecamp.org/news/javascript-debounce-example/

keys and values in a JS object

https://javascript.info/keys-values-entries

Understanding offsetWidth, clientWidth, scrollWidth

https://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively

Get access to all 537 Bricks code tutorials with BricksLabs Pro

3 comments

  • The "More" button doesn't seem to work

  • Aarón Blanco Tejedor

    Hello Maxime ????

    I am about to use this method in my second website, but this time the header doesn't have a button on it, and when not using a button, as soon as there is no space available, all the elements collapse under the "more" element. Do you think it would be possible to make this work without the button element?

    Love ????

  • Stephen Revere

    Wow. Glad I got the pro version early!

Leave your comment

 

Related Tutorials..

Pro
Filtering Masonry-layout Posts by Categories in Bricks using Isotope

Filtering Masonry-layout Posts by Categories in Bricks using Isotope

Step by step on how to use Isotope in Bricks for filtering posts by categories.
Categories:
Pro
Meta Box/ACF Images Staggered Grid Gallery in Bricks

Meta Box/ACF Images Staggered Grid Gallery in Bricks

Showing images from two Gallery-type of ACF or Image Advanced-type of Meta Box fields in a staggered grid layout.
Pro
Top Bar Above Sticky Header in Bricks

Top Bar Above Sticky Header in Bricks

How to set up a top bar above sticky header in Bricks Builder so only the header remains sticky when scrolling.
Categories:
Tags:
Create a App-Like Navigation in Bricks

Create a App-Like Navigation in Bricks

I've promised a few folks in the Facebook group to create a tutorial on how to create an app-like navigation in Bricks like in the…
Categories:
How to create filters with IsotopeJS in Bricks (Part 1)

How to create filters with IsotopeJS in Bricks (Part 1)

This tutorial series will explore the IsotopeJS library's features inside the Bricks ecosystem.
Categories:
How to add SVG icon as list item bullets

How to add SVG icon as list item bullets

Add this sample CSS: Replace brxe-list with the class of your ul element. Here's how you can generate the value of background-image property: Copy your…
Categories:
Tags:
Pro
Menu Item Descriptions in Bricks

Menu Item Descriptions in Bricks

Updated on 17 Jul 2023 This Pro tutorial provides the steps to show descriptions for nav menu items with the corresponding CSS styling for desktops…
Pro
Nav Menus Custom Query Types in Bricks

Nav Menus Custom Query Types in Bricks

Updated on 21 Aug 2024 This Pro tutorial shows how custom query types for each navigation menu can be generated in Bricks. This enables us…