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.
menu-wrapper
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.
menu-inner
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
3 comments
Oleh
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!