如何用JavaScript实现鼠标悬停时保持子菜单展开
解决悬停显示子菜单且点击子菜单不消失的问题
首先咱们分析下问题根源:当你把鼠标从父元素.item移到子菜单.submenu时,如果两者之间存在间隙,或者父元素的hover状态没覆盖到子菜单区域,就会导致父元素hover失效,子菜单跟着消失。另外你原有的JavaScript点击事件,也可能在点击子菜单时意外触发关闭逻辑。
下面给你两种可行的解决方案:
方案一:纯CSS实现(推荐,简洁高效)
利用CSS的hover伪类特性——当鼠标停留在子元素上时,父元素的hover状态会持续保持,这样就能保证子菜单不会消失。同时调整子菜单的定位,确保和父元素无缝衔接,避免鼠标移动时出现间隙。
修改后的CSS代码
/* 默认隐藏子菜单 */ .submenu { display: none; position: absolute; left: 0; top: 100%; /* 子菜单顶部紧贴父元素底部,消除间隙 */ min-width: 200px; margin: 0; /* 移除默认margin,避免产生间隙 */ padding: 0; /* 可选:移除默认内边距,统一布局 */ } /* 父元素hover时显示子菜单 */ .item:hover .submenu { display: block; } /* 给父元素设置相对定位,让子菜单的绝对定位基于它 */ .item { position: relative; }
为什么这能解决问题?
- 当鼠标悬停在
.item上,子菜单显示;移到子菜单上时,因为子菜单是.item的子元素,.item的hover状态会一直保持,子菜单自然不会消失。 position: relative给子菜单的绝对定位提供了正确的参考容器,确保子菜单位置精准。- 移除默认边距+设置
top:100%,彻底避免父元素和子菜单之间出现间隙,防止鼠标移动时意外跳出父元素的hover范围。
方案二:结合JavaScript优化(适配移动端点击需求)
如果你需要同时支持移动端的点击触发(移动端没有hover效果),可以保留JS逻辑,但要修改事件监听,避免点击子菜单时误关父菜单。
修改后的JavaScript代码
const items = document.querySelectorAll(".item"); const menu = document.querySelector(".menu"); // 切换子菜单显示状态 function toggleItem(e) { // 阻止事件冒泡,防止点击子菜单时触发父元素的事件 e.stopPropagation(); // 只有点击父元素的触发区域(而非子菜单)时,才切换状态 if (!e.target.closest(".submenu")) { if (this.classList.contains("submenu-active")) { this.classList.remove("submenu-active"); } else { // 先关闭其他已打开的子菜单 menu.querySelector(".submenu-active")?.classList.remove("submenu-active"); this.classList.add("submenu-active"); } } } // 鼠标进入父元素时打开子菜单 function handleMouseEnter() { // 关闭其他打开的子菜单 menu.querySelector(".submenu-active")?.classList.remove("submenu-active"); this.classList.add("submenu-active"); } // 鼠标离开时延迟关闭,给用户移到子菜单的缓冲时间 function handleMouseLeave() { setTimeout(() => { // 只有当鼠标完全离开父元素和子菜单时,才关闭 if (!this.matches(":hover") && !this.querySelector(".submenu:hover")) { this.classList.remove("submenu-active"); } }, 100); } // 给带子菜单的父元素绑定事件 for (let item of items) { if (item.querySelector(".submenu")) { item.addEventListener("click", toggleItem, false); item.addEventListener("mouseenter", handleMouseEnter, false); item.addEventListener("mouseleave", handleMouseLeave, false); // 给父元素的触发链接添加focus事件,支持键盘导航 item.querySelector("a[tabindex='0']").addEventListener("focus", () => { handleMouseEnter.call(item); }); } }
对应的CSS调整
.submenu { display: none; position: absolute; left: 0; top: 100%; min-width: 200px; margin: 0; } /* 通过类控制子菜单显示,兼容JS逻辑 */ .item.submenu-active .submenu { display: block; } .item { position: relative; }
关键优化点
- 用
e.stopPropagation()阻止点击子菜单时事件冒泡到父元素,避免误触发关闭逻辑。 - 鼠标离开时添加100ms延迟,给用户足够时间把鼠标移到子菜单上,防止子菜单突然消失。
- 离开前检查鼠标位置,只有完全脱离父元素和子菜单区域时才关闭子菜单。
额外注意事项
- 一定要给
.item设置position: relative,否则子菜单的绝对定位会基于整个文档,位置会错乱。 - 可以移除子菜单默认的
padding和margin,让布局更规整,减少间隙风险。 - 如果需要支持键盘导航,记得给子菜单的链接设置合适的
tabindex,保证页面可访问性。
内容的提问来源于stack exchange,提问作者Arthur Fortin
相关产品推荐
相关产品推荐

