boxmoe_header_banner_img

Hello! 欢迎来到悠悠畅享网!

文章导读

使用纯 JavaScript 实现响应式多级下拉菜单:悬停与点击的结合


avatar
作者 2025年9月9日 9

使用纯 JavaScript 实现响应式多级下拉菜单:悬停与点击的结合

本文旨在指导开发者如何使用原生 JavaScript、htmlcss 构建一个响应式的多级下拉菜单。该菜单在桌面端采用悬停触发,而在移动端则切换为点击触发模式,无需依赖 jquery 库。我们将重点解决移动端点击事件无法正确展开子菜单的问题,并提供代码示例和优化建议,帮助读者构建用户体验良好的导航菜单。

HTML 结构

首先,我们来看一下 HTML 的基本结构。一个 div 元素作为菜单容器,内部包含一个无序列表 ul,列表项 li 可以包含链接 a 和嵌套的子列表 ul。

<div class="menu">   <ul>     <li>       Parent 1       <ul>         <li class="link">           <a href="">Child 1</a>         </li>         <li class="link">           <a href="">Child 2</a>         </li>         <li>           Nested child           <ul>             <li class="link">               <a href="">Something</a>             </li>             <li class="link">               <a href="">Something</a>             </li>           </ul>         </li>       </ul>     </li>     <li>       Parent 2       <ul>         <li class="link">           <a href="">Basic link 1</a>         </li>         <li class="link">           <a href="">Basic link 2</a>         </li>         <li>           Nested child 2           <ul>             <li class="link">               <a href="">Basic nested link 1</a>             </li>             <li class="link">               <a href="">Basic nested link 2</a>             </li>            </ul>         </li>       </ul>     </li>     <li class="link">Simple Link</li>     <li class="link">Another Link</li>   </ul> </div>

CSS 样式

接下来,我们定义 CSS 样式。关键在于实现悬停效果和移动端的显示/隐藏逻辑。通过媒体查询 @media 来区分桌面端和移动端。

.menu {   --menu-height: 40px;   box-sizing: border-box;   position: fixed;   top: 0;   left: 0;   width: 100vw; } .menu ul {   list-style: none;   padding: 16px;   margin: 0; } .menu ul li, .menu ul li a {   opacity: 0.8;   color: #ffffff;   cursor: pointer;   transition: 200ms;   text-decoration: none;   white-space: nowrap;   font-weight: 700; } .menu ul li a, .menu ul li a a {   display: flex;   align-items: center;   height: 100%;   width: 100%; } .menu ul li {   padding-right: 36px; } .menu ul li::before {   content: "";   width: 0;   height: 0;   border-left: 5px solid transparent;   border-right: 5px solid transparent;   border-top: 5px solid #ffa500;   position: absolute;   right: 8px;   top: 50%;   transform: translateY(-50%); } .menu ul .link::before {   padding-right: 0;   display: none; } .menu > ul {   display: flex;   height: var(--menu-height);   align-items: center;   background-color: #000000; } .menu > ul li {   position: relative;   margin: 0 8px; } .menu > ul li ul {   visibility: hidden;   opacity: 0;   padding: 0;   min-width: 160px;   background-color: #333;   position: absolute;   top: 45px;   left: 50%;   transform: translateX(-50%);   transition: 200ms;   transition-delay: 200ms; } .menu > ul li ul li {   margin: 0;   padding: 8px 16px;   display: flex;   align-items: center;   justify-content: flex-start;   height: 30px;   padding-right: 40px; } .menu > ul li ul li::before {   width: 0;   height: 0;   border-top: 5px solid transparent;   border-bottom: 5px solid transparent;   border-left: 5px solid #ffa500; } .menu > ul li ul li ul {   top: -2%;   left: 100%;   transform: translate(0); }  .show {   display: flex; }  .hide {   display: none; }  @media screen and (min-width: 1200px) {   .menu ul li:hover,   .menu ul li a:hover {     opacity: 1;   }   .menu > ul li ul li:hover {     background: black;   }   .menu > ul li:hover > ul {     opacity: 1;     visibility: visible;     transition-delay: 0ms;   } }  @media screen and (max-width: 1200px) {   .menu {     height: 100vh;     top: 0;     left: 0;     width: 20vw;   }   .menu ul {     flex-direction: column;     height: 100vh;   }   .menu ul li {     margin: 1rem 0;   }   .menu ul li ul {     visibility: hidden;     opacity: 0;     padding: 0;     min-width: 160px;     background-color: #333;     position: absolute;     top: 0px;     left: 200%;     transform: translateX(-50%);     transition: 200ms;     transition-delay: 200ms;     height: fit-content;   }    .menu ul.show {     visibility: visible;     opacity: 1;   } }

关键点:

  • 桌面端: 使用 :hover 伪类控制子菜单的显示和隐藏 (opacity: 1; visibility: visible;)。
  • 移动端: 初始状态下,子菜单 ul 的 visibility 设置为 hidden 和 opacity 设置为 0。 通过添加 .show 类来改变 visibility 和 opacity。

JavaScript 交互

JavaScript 代码负责处理移动端的点击事件,切换子菜单的显示状态。

let menu = document.querySelector(".menu ul"); menu = menu.children;  for (let i = 0; i < menu.length; i++) {   console.log(menu[i]);   menu[i].addEventListener("click", function() {     menu[i].firstElementChild.classlist.toggle("show");     console.log(menu[i].firstElementChild);   }); }

这段代码存在一个问题,它直接尝试给每个 li 元素添加点击事件,并切换其第一个子元素的 show 类。 这会导致只有第一层子菜单能够正常工作,更深层次的子菜单无法正确展开。

使用纯 JavaScript 实现响应式多级下拉菜单:悬停与点击的结合

Fotor AI Image Upscaler

Fotor推出的AI图片放大工具

使用纯 JavaScript 实现响应式多级下拉菜单:悬停与点击的结合48

查看详情 使用纯 JavaScript 实现响应式多级下拉菜单:悬停与点击的结合

正确的做法是,首先判断当前是否是移动端,如果是,则添加点击事件,否则不添加。 并且,需要针对所有层级的 li 元素添加事件监听,并阻止事件冒泡,避免父元素和子元素同时触发点击事件。

以下是改进后的 JavaScript 代码:

document.addEventListener('domContentLoaded', function() {   const menuItems = document.querySelectorAll('.menu ul li');   const mediaQuery = window.matchMedia('(max-width: 1200px)');    function handleMenuItemClick(event) {     // 阻止事件冒泡,防止父元素同时触发     event.stopPropagation();      const subMenu = this.querySelector('ul');     if (subMenu) {       subMenu.classList.toggle('show');     }   }    function attachEventListeners(matches) {     menuItems.forEach(item => {       item.removeEventListener('click', handleMenuItemClick); // 移除之前的监听器        if (matches) {         item.addEventListener('click', handleMenuItemClick);       }     });   }    // 初始化时执行一次   attachEventListeners(mediaQuery.matches);    // 监听媒体查询变化   mediaQuery.addEventListener('change', (event) => {     attachEventListeners(event.matches);   }); });

代码解释:

  1. DOMContentLoaded 事件: 确保在 DOM 加载完成后执行 JavaScript 代码。
  2. querySelectorAll(‘.menu ul li’): 获取所有菜单项。
  3. window.matchMedia(‘(max-width: 1200px)’): 创建一个媒体查询对象,用于检测当前屏幕是否是移动端。
  4. handleMenuItemClick(event): 点击事件处理函数。
    • event.stopPropagation(): 阻止事件冒泡,防止点击子菜单时,父菜单也触发点击事件。
    • this.querySelector(‘ul’): 查找当前菜单项下的子菜单。
    • subMenu.classList.toggle(‘show’): 切换子菜单的 show 类,控制其显示和隐藏。
  5. attachEventListeners(matches): 根据媒体查询结果,添加或移除点击事件监听器。
    • item.removeEventListener(‘click’, handleMenuItemClick):在添加新的监听器之前,先移除之前的监听器,避免重复绑定。
    • item.addEventListener(‘click’, handleMenuItemClick):添加点击事件监听器。
  6. mediaQuery.addEventListener(‘change’, (event) => { … }): 监听媒体查询的变化,当屏幕尺寸改变时,重新添加或移除点击事件监听器。

优化建议

  • 语义化 HTML: 可以使用 <nav> 元素包裹菜单,使用 aria-label 属性增强可访问性。
  • CSS 动画: 可以使用 CSS 动画来增强菜单的过渡效果,提升用户体验。
  • 性能优化: 如果菜单项非常多,可以考虑使用事件委托来减少事件监听器的数量。
  • 使用 <button> 替代 <span>: 对于可点击的菜单项,使用 <button> 元素可以更好地表达其交互行为,并且更易于使用键盘进行导航。

总结

通过以上步骤,我们实现了一个响应式的多级下拉菜单,可以在桌面端使用悬停,在移动端使用点击进行交互。 关键在于 CSS 样式的媒体查询和 JavaScript 的事件处理。 通过不断优化,可以创建一个用户体验良好的导航菜单。



评论(已关闭)

评论已关闭