🆕 Bricks 2.4 已发布:查询循环性能提升 40%,迁移教程同步更新 →

首页 / 中文教程 / 教程

Bricks 自适应桌面导航栏(溢出自动收纳到 More)

教程:Bricks 自适应桌面导航栏(溢出自动收纳到 More)——Bricks 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。

Ray ChanRay Chan·2026-08-12·约 4 分钟
目录
  1. 1.引言
  2. 2.DOM 结构
  3. 3.JavaScript
  4. 4.CSS
  5. 5.参考资源
  6. 6.结论

这篇教程教你:当桌面端导航栏溢出时,自动添加一个「More」菜单项。

引言

UI/UX 文献都建议导航菜单项越少越好。但做 Web 的人都知道,总有些客户坚持要在导航栏里塞一堆菜单项,结果为了在各种断点上避免溢出,样式调起来简直是噩梦。

其实有另一种办法:与其写一堆媒体查询,不如集成这段小脚本。只要你的菜单在窗口缩放时发生溢出,脚本就会自动在导航栏末尾创建一个「More」项,并把溢出的菜单项收进它的子菜单里。来看看怎么实现。

DOM 结构

按照下面的 DOM 树搭建:

一定要按上图把 menu-wrapper、menu-inner、menu-crop 这三个类加到正确的元素上。

menu-wrapper 元素需要 100% 宽度。如果你想要盒装(boxed)导航栏,再加上 max-width:

menu-inner 块需要启用 flexbox,并使用 flex-direction: row:

JavaScript

把下面的 JavaScript 代码复制/粘贴到需要生效的地方。举个例子,如果你的菜单是全局菜单、所有页面都显示,可以把代码粘贴到 Bricks Settings → Custom Code 标签页 → 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

把下面的 CSS 片段粘贴到脚本运行的地方。同样,全局菜单可以放在 Bricks Settings → Custom Code 标签页 → 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;
}

参考资源

debounce 函数(防抖)

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

JS 对象中的键值对

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

理解 offsetWidth、clientWidth、scrollWidth

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

相关阅读:

结论

脚本的核心逻辑:用 scrollWidth > clientWidth 判断 menu-inner 是否溢出;溢出就从菜单末尾逐个摘掉菜单项收进 #moreSubMenu 子菜单,并创建带数量徽章(#moreQty)的「More」项;窗口拉宽不再溢出时再按逆序放回。resize 事件用防抖(默认阈值 200ms)避免频繁计算。搭配 CSS 里的 white-space: nowrap 防止菜单项换行即可。

常见误区

几个坑:一是滚动监听没做防抖,每次滚动都重算宽度,掉帧卡顿,按本文加 debounce;二是只监听 window 宽度却没算实际溢出(offsetWidth/scrollWidth),判断不准;三是移动端忘了关这个效果,小屏上导航忽大忽小很怪。可缩放导航条是桌面端精致交互,移动端建议直接走固定/汉堡菜单,别在小屏上硬套。

延伸阅读

常见问题(FAQ)

为什么要用 debounce? 滚动监听每次触发都重算宽度会掉帧,加防抖让计算节流,滚动更顺。

offsetWidth 和 scrollWidth 差在哪? offsetWidth 是可见宽,scrollWidth 是内容实际宽;判断溢出要看 scrollWidth 是否超过 offsetWidth。

移动端要这个效果吗? 不建议,小屏上导航缩放很怪,移动端走固定或汉堡菜单更稳。

会影响性能吗? 不加防抖会,加上 debounce 并把计算限制在桌面断点内就基本无感。

Ray Chan

站长

Ray Chan

WordPress Developer & Bricks Specialist

WordPress developer with 10+ years of client builds. Switched to Bricks in 2023 — now builds fast WordPress sites and migrates legacy Elementor/Divi projects.

延伸阅读