HTML菜单源码实战:jQuery多级下拉与手风琴折叠组件开发 简介这份源码包面向前端初学者与需要快速搭建导航栏的开发者汇集了多套美观实用的HTML菜单实现方案涵盖纯CSS基础菜单到结合JavaScript与jQuery的交互式下拉菜单可用于个人项目、企业官网或后台系统的导航模块开发。压缩包共38个文件约481KB以less与scss样式源文件为主便于按需修改变量、混入与图标样式另含3个js脚本、2个css文件及Font Awesome字体图标资源并附带一个html示例页面方便直接预览效果。目前已有2596人学习下载热度较高。读者可从中获取完整的菜单结构模板、响应式布局思路与动画交互写法通过阅读样式分层与脚本逻辑快速掌握下拉展开、状态切换等常见效果的实现方式也可将其作为项目脚手架直接复用节省从零编写导航组件的时间。1. 从一份菜单源码说起为什么前端老手也愿意收藏这套 HTML 菜单前阵子帮一个做后台管理系统的朋友改界面他丢过来一份 HTML 菜单源码说“你帮我看看这个能不能直接塞进项目里”。我打开一看纯静态的 HTML CSS 少量 jQuery没有构建工具没有 npm 依赖双击 index.html 就能跑。菜单项带多级下拉、手风琴折叠、移动端汉堡按钮配色和动效做得相当克制不是那种花里胡哨的模板站风格。这种“拿来就能用”的菜单组件在实际项目里其实比想象中稀缺——大部分开源菜单要么绑死在某个框架上要么样式需要大改才能融入现有设计。这份源码的价值在于它把 HTML 菜单、JS 交互、jQuery 事件委托、CSS 过渡动画这几块拆得比较清楚新手能直接改文字和链接熟手能顺着结构替换成自己的路由或权限逻辑。如果你正在找一个不依赖框架、能快速嵌入后台或官网的菜单方案这份源码值得花半小时拆一遍。2. 项目结构与技术选型菜单是怎么被组织起来的2.1 目录结构静态资源的分层逻辑拿到一份源码我习惯先看目录因为目录结构基本暴露了作者的拆分思路。这份 HTML 菜单源码的典型结构如下menu-project/ ├── index.html # 主页面包含菜单的 HTML 骨架 ├── css/ │ ├── reset.css # 基础样式重置 │ ├── menu.css # 菜单核心样式布局、配色、动效 │ └── responsive.css # 移动端适配样式 ├── js/ │ ├── jquery.min.js # jQuery 库文件 │ └── menu.js # 菜单交互逻辑下拉、折叠、高亮 ├── images/ │ └── arrow.svg # 下拉箭头图标 └── fonts/ # 图标字体部分版本使用这个分层不算复杂但有几个点值得注意。menu.css和responsive.css分开说明作者考虑了桌面端和移动端的不同表现而不是用一套媒体查询硬塞。menu.js独立于 HTML意味着交互逻辑可以单独调试也方便替换成原生 JS 或 Vue 指令。images里只放了一个箭头 SVG说明图标尽量用矢量或字体减少图片请求。这种组织方式对二次开发很友好改样式只动 CSS改交互只动 JS改结构只动 HTML三者边界清晰。提示如果你打算把菜单嵌入现有项目建议保留menu.css和menu.js的独立性不要急着合并到全局文件里否则后期排查样式冲突会很痛苦。2.2 技术选型为什么是 jQuery 而不是原生 JS这份源码用了 jQuery这在 2024 年看起来有点“复古”但放在菜单组件这个场景里其实有它的合理性。菜单交互的核心是事件委托和 DOM 操作点击父菜单展开子菜单、点击外部关闭、鼠标悬停切换高亮。jQuery 的on()方法天然支持事件委托写法比原生addEventListener简洁不少。比如下面这段菜单初始化的代码// menu.js $(document).ready(function () { // 事件委托绑定到菜单容器处理所有菜单项的点击 $(.menu-container).on(click, .menu-item.has-children a, function (e) { e.preventDefault(); // 阻止链接默认跳转 var $parent $(this).parent(); // 关闭同级其他展开的菜单 $parent.siblings(.has-children).removeClass(open) .find(.submenu).slideUp(200); // 切换当前菜单的展开状态 $parent.toggleClass(open); $parent.find(.submenu).slideToggle(200); }); // 点击菜单外部区域关闭所有下拉 $(document).on(click, function (e) { if (!$(e.target).closest(.menu-container).length) { $(.menu-item.has-children).removeClass(open) .find(.submenu).slideUp(200); } }); });这段代码的逻辑说明第一段用事件委托监听.menu-item.has-children a的点击e.preventDefault()阻止链接跳转然后先关闭同级其他展开项再切换当前项的.open类和子菜单的滑动显示。第二段监听整个文档的点击如果点击目标不在菜单容器内就关闭所有下拉。参数方面slideUp(200)和slideToggle(200)里的 200 是动画时长单位毫秒改成 0 就是瞬间显示改成 400 会更慢。.has-children和.open是自定义类名改的时候要同步改 CSS 里的选择器。用 jQuery 的另一个好处是兼容性。虽然现在主流浏览器对原生 JS 支持已经很好了但后台系统经常要兼容一些老旧环境jQuery 能省掉不少 polyfill 的麻烦。当然如果你项目里已经全面拥抱 Vue 或 React完全可以把这段逻辑改写成组件事件后面二次开发章节会讲怎么改。2.3 HTML 骨架菜单的语义化结构菜单的 HTML 结构决定了可访问性和 SEO 友好度。这份源码用的是嵌套ulli的经典写法nav classmenu-container ul classmenu li classmenu-itema href/首页/a/li li classmenu-item has-children a href#产品中心/a ul classsubmenu lia href/product/a产品 A/a/li lia href/product/b产品 B/a/li li classhas-children a href#更多产品/a ul classsubmenu lia href/product/c产品 C/a/li /ul /li /ul /li li classmenu-itema href/about关于我们/a/li /ul /nav这里的关键类是has-children它标记了有子菜单的项JS 和 CSS 都依赖这个类来判断是否显示下拉箭头、是否绑定展开逻辑。submenu是子菜单的容器支持多层嵌套。nav标签给屏幕阅读器提供了语义信息。如果你要改成动态渲染比如从后端接口拿菜单数据只需要保证生成的 DOM 结构里带上这些类名即可。3. 跑起来从双击 index.html 到嵌入项目3.1 本地运行零依赖启动这份源码最大的好处就是不需要任何构建工具。你拿到文件夹后直接双击index.html浏览器就会打开菜单页面。如果菜单样式或交互没生效按 F12 打开控制台看两个地方一是 Network 面板里menu.css和menu.js是否 200 加载成功二是 Console 面板有没有报错。常见问题是路径写错比如css/menu.css写成了./css/menu.css在某些服务器环境下会 404。本地文件协议下一般没问题但如果你用 VS Code 的 Live Server 插件启动路径规则会更严格。如果你习惯用命令行可以在项目根目录跑一个静态服务器# Python 3 自带 http.server适合快速验证 python -m http.server 8080 # 或者用 Node 的 http-server需要先 npm install -g http-server http-server -p 8080启动后访问http://localhost:8080菜单应该正常显示。用本地服务器的好处是路径解析和真实部署环境一致能提前发现相对路径的问题。3.2 嵌入现有项目三个必须检查的接入点把菜单嵌入现有项目时不能直接复制整个文件夹要按模块拆开接入。我一般会检查三个接入点第一CSS 冲突。菜单用了.menu、.submenu、.open这些通用类名很容易和项目里已有的样式撞车。解决办法是给菜单容器加一个唯一前缀比如.myapp-menu然后批量替换 CSS 和 JS 里的选择器。下面是一个批量替换的示例# 在 menu.css 和 menu.js 里把 .menu 替换成 .myapp-menu # 注意不要替换 .submenu 里的 menu 子串用更精确的匹配 sed -i s/\.menu-container/.myapp-menu-container/g css/menu.css js/menu.js sed -i s/\.menu-item/.myapp-menu-item/g css/menu.css js/menu.js sed -i s/\.submenu/.myapp-submenu/g css/menu.css js/menu.js第二JS 加载顺序。menu.js依赖 jQuery所以script标签的顺序必须是 jQuery 在前、menu.js 在后。如果项目用了模块打包工具要确保 jQuery 被正确引入或替换成原生实现。第三HTML 插入位置。菜单通常放在页面顶部或侧边栏插入时注意不要破坏原有布局的 flex 或 grid 结构。如果原布局是 flex 容器菜单的nav可能需要设置flex-shrink: 0防止被压缩。3.3 移动端适配汉堡菜单的触发逻辑这份源码的移动端适配用的是媒体查询加汉堡按钮。在responsive.css里当屏幕宽度小于 768px 时菜单默认隐藏显示一个汉堡图标点击图标后菜单以侧滑或下拉方式展开。对应的 JS 逻辑通常是// 移动端汉堡按钮切换 $(.menu-toggle).on(click, function () { $(.menu-container).toggleClass(mobile-open); // 切换按钮图标状态可选 $(this).toggleClass(active); });这里的.menu-toggle是汉堡按钮的类名.mobile-open控制菜单的显示隐藏。CSS 里.mobile-open通常配合transform: translateX(0)或max-height过渡来实现动画。参数方面媒体查询的断点 768px 可以根据项目实际设计稿调整常见断点还有 992px 和 1200px。如果你用的是 Bootstrap 之类的框架可以直接复用它的断点变量。注意移动端菜单展开后建议给body加一个overflow: hidden防止背景页面滚动这个细节很多源码会忽略但实际体验差别很大。4. 核心交互拆解下拉、折叠、高亮是怎么实现的4.1 多级下拉slideToggle 与类名切换的配合多级下拉的核心是slideToggle和.open类的配合。slideToggle负责动画.open负责状态标记CSS 里通过.open .submenu控制箭头旋转或背景色变化。这种“JS 管状态、CSS 管表现”的模式比纯 JS 改样式更易维护。下面是一个带箭头旋转的 CSS 片段/* 下拉箭头默认向下 */ .menu-item.has-children a::after { content: ; display: inline-block; width: 6px; height: 6px; border-right: 2px solid #666; border-bottom: 2px solid #666; transform: rotate(45deg); transition: transform 0.2s; margin-left: 6px; } /* 展开时箭头向上 */ .menu-item.open a::after { transform: rotate(-135deg); }这段 CSS 用伪元素画了一个箭头transform: rotate(45deg)是向下rotate(-135deg)是向上配合transition实现平滑旋转。参数上border的粗细和颜色可以按设计稿调margin-left控制箭头和文字的间距。如果你不想用伪元素也可以直接放一个i标签或 SVG 图标改起来更直观。4.2 手风琴折叠同级互斥的实现细节手风琴效果的关键是“同级互斥”——展开一个菜单时自动关闭它的兄弟菜单。这在第 2 章的 JS 代码里已经体现了$parent.siblings(.has-children).removeClass(open).find(.submenu).slideUp(200)。这里siblings()选中同级元素removeClass(open)清除状态find(.submenu)找到子菜单并收起。注意slideUp和slideToggle的区别slideUp只收起不展开slideToggle根据当前状态切换。在手风琴场景里关闭兄弟菜单用slideUp更明确避免误触发展开。如果菜单层级很深比如三级以上siblings()只会处理同一父级下的兄弟不会影响其他层级这是符合预期的。但要注意如果 HTML 结构里.submenu不是直接子元素find()可能会选中嵌套更深的菜单导致意外收起。稳妥的做法是用children(.submenu)限定直接子级。4.3 当前项高亮根据 URL 自动匹配菜单高亮是后台系统的刚需——用户要知道自己当前在哪个页面。这份源码的高亮逻辑通常是遍历所有菜单链接对比href和当前location.pathname// 根据当前 URL 高亮菜单项 $(function () { var currentPath window.location.pathname; $(.menu a).each(function () { var linkPath $(this).attr(href); // 简单匹配路径完全相等或以当前路径开头 if (linkPath currentPath || (linkPath ! / currentPath.indexOf(linkPath) 0)) { $(this).addClass(active); // 如果高亮项在子菜单里展开父级菜单 $(this).parents(.has-children).addClass(open) .children(.submenu).show(); } }); });这段代码的逻辑说明currentPath拿到当前页面路径遍历所有菜单链接如果链接的href和当前路径匹配完全相等或作为前缀就给链接加.active类。如果高亮项藏在子菜单里还要向上查找.has-children父级展开它。参数上indexOf(linkPath) 0是前缀匹配适合/product/a和/product/a/detail这种场景如果要求精确匹配改成即可。.active类的样式在 CSS 里定义通常是加粗、变色或加左边框。提示如果项目用了前端路由如 Vue Router、React Routerwindow.location.pathname可能不会随路由变化而更新需要监听路由变化事件或者直接用路由库提供的当前路由信息来高亮。5. 避坑与常见问题菜单不生效、样式错乱、移动端点击穿透5.1 菜单点击没反应控制台报$ is not defined现象页面加载后点击菜单项没有任何反应F12 控制台显示Uncaught ReferenceError: $ is not defined。原因jQuery 没有加载或者menu.js在 jQuery 之前执行了。解决检查script标签顺序确保jquery.min.js在menu.js之前如果用了defer或async要确认执行顺序如果项目里已经有一个 jQuery 版本不要重复引入避免版本冲突。5.2 下拉菜单被父容器overflow: hidden裁掉现象子菜单展开后显示不全被截断了一部分。原因菜单的某个父级容器设置了overflow: hidden子菜单超出容器范围就被裁剪。解决找到设置overflow: hidden的父级改成overflow: visible如果父级必须隐藏溢出比如圆角容器可以把菜单用position: absolute定位到body下或者用position: fixed脱离文档流。常见做法是给菜单容器加z-index和position: relative确保下拉层级在最上面。5.3 移动端点击菜单后背景页面跟着滚动现象在手机上展开菜单后滑动菜单区域背景页面也跟着滚。原因菜单展开时没有锁定body滚动。解决在菜单展开的回调里给body加overflow: hidden关闭时移除。如果菜单本身需要滚动给菜单容器设overflow-y: auto和max-height。注意 iOS 上overflow: hidden有时不生效可以同时加position: fixed; width: 100%到body上关闭时恢复。5.4 多个菜单实例互相干扰现象页面上放了两个菜单比如顶部导航和侧边栏点击一个菜单另一个也跟着展开。原因JS 选择器用了全局的.menu-item没有限定在各自的容器内。解决给每个菜单容器加唯一 ID 或类名JS 里用$(#menu-a .menu-item)限定范围。如果用的是事件委托确保绑定到各自的容器上而不是document。5.5 样式在 IE 或旧版浏览器上错乱现象菜单在 Chrome 上正常在旧版浏览器上布局错乱、动画失效。原因用了 CSS3 的flex、transition、transform等属性旧版浏览器支持不完整。解决如果必须兼容旧版用float或table布局替代flex动画降级为直接显示隐藏引入html5shiv和respond.js处理 HTML5 标签和媒体查询兼容。不过现在大部分项目已经不需要兼容 IE 了这条更多是给维护老系统的开发者提个醒。6. 二次开发把 jQuery 菜单改造成 Vue 组件或原生 JS 模块6.1 改造思路从 DOM 操作到数据驱动这份源码的交互逻辑是“命令式”的——直接操作 DOM、切换类名。如果你项目用 Vue 或 React更合适的做法是把菜单数据抽成 JSON用数据驱动渲染。下面是一个 Vue 3 的改造示例template nav classmyapp-menu-container ul classmyapp-menu li v-foritem in menuData :keyitem.id :class[myapp-menu-item, { has-children: item.children, open: openIds.includes(item.id) }] a :hrefitem.link click.preventtoggle(item){{ item.title }}/a ul v-ifitem.children classmyapp-submenu v-showopenIds.includes(item.id) li v-forchild in item.children :keychild.id a :hrefchild.link{{ child.title }}/a /li /ul /li /ul /nav /template script export default { data() { return { menuData: [ { id: 1, title: 首页, link: / }, { id: 2, title: 产品中心, link: #, children: [ { id: 21, title: 产品 A, link: /product/a }, { id: 22, title: 产品 B, link: /product/b } ]} ], openIds: [] // 存储展开的菜单 ID }; }, methods: { toggle(item) { if (!item.children) return; const index this.openIds.indexOf(item.id); if (index -1) { this.openIds.splice(index, 1); // 收起 } else { this.openIds.push(item.id); // 展开 } } } }; /script改造的关键点把 HTML 里的菜单项抽成menuData数组用v-for渲染用openIds数组管理展开状态替代 jQuery 的.open类toggle方法只改数据DOM 由 Vue 自动更新。CSS 部分可以直接复用原来的menu.css只需要把类名前缀统一替换。这种改造的好处是菜单状态可预测、可测试也方便从后端接口动态加载菜单数据。6.2 原生 JS 模块化去掉 jQuery 依赖如果不想引入 Vue也可以把 jQuery 逻辑改写成原生 JS 模块。核心是把$(selector)换成document.querySelector把on()换成addEventListener把slideToggle换成 CSS 过渡加类名切换。下面是一个原生 JS 的菜单初始化函数// menu-native.js export function initMenu(containerSelector) { const container document.querySelector(containerSelector); if (!container) return; // 事件委托点击菜单项 container.addEventListener(click, (e) { const link e.target.closest(.has-children a); if (!link) return; e.preventDefault(); const parent link.parentElement; // 关闭同级其他菜单 parent.parentElement.querySelectorAll(.has-children.open).forEach((el) { if (el ! parent) { el.classList.remove(open); const sub el.querySelector(.submenu); if (sub) sub.style.maxHeight null; } }); // 切换当前菜单 parent.classList.toggle(open); const submenu parent.querySelector(.submenu); if (submenu) { if (parent.classList.contains(open)) { submenu.style.maxHeight submenu.scrollHeight px; } else { submenu.style.maxHeight null; } } }); // 点击外部关闭 document.addEventListener(click, (e) { if (!container.contains(e.target)) { container.querySelectorAll(.has-children.open).forEach((el) { el.classList.remove(open); const sub el.querySelector(.submenu); if (sub) sub.style.maxHeight null; }); } }); }这段代码用max-height过渡替代slideToggle因为原生 JS 没有内置的滑动动画。scrollHeight拿到子菜单的实际高度赋给max-height实现展开动画收起时设为nullCSS 里transition: max-height 0.2s负责过渡。注意max-height的值不能设太小否则内容会被截断用scrollHeight动态计算最稳妥。这个模块可以导出成 ES module在项目入口处调用initMenu(.myapp-menu-container)即可。6.3 验证改造效果三个必测场景改造完成后我一般会跑三个场景验证第一桌面端点击父菜单子菜单展开再点收起动画流畅无卡顿第二移动端宽度下汉堡按钮能正常切换菜单显示展开后背景不滚动第三直接访问一个子菜单的 URL比如/product/a刷新页面后菜单自动展开并高亮对应项。这三个场景覆盖了菜单的核心交互如果都通过基本可以放心上线。从那以后我每次拿到这类静态菜单源码都会先跑一遍这三个场景再决定是直接嵌入还是改造成组件。这套流程帮我省了不少返工时间希望帮到你。本文还有配套的精品资源点击获取