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

Instant access to all 250+ Bricks code tutorials with BricksLabs Pro

2 comments

  • 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