Bootstrap 5 默认禁用悬停下拉,仅支持点击触发,因兼顾移动端与可访问性;若需悬停,须手动添加防抖JS、键盘焦点支持及移动端降级逻辑。

Bootstrap 5 默认不支持悬停触发下拉菜单
Bootstrap 5 官方移除了 data-bs-toggle="hover" 和所有 hover 相关的 JS 行为,下拉菜单(Dropdown)默认仅响应点击(click)事件。如果你在页面上写了个二级菜单,鼠标移入一级项就期望二级展开,但实际要点击才弹出——这不是你代码写错了,是 Bootstrap 5 故意这么设计的。
原因很实际:移动端没有 hover 概念,强制 hover 会破坏可访问性和触屏体验;另外,纯 CSS 实现的 hover 下拉在键盘导航、屏幕阅读器下几乎不可用。
- 官方明确建议「不要为下拉菜单添加悬停行为」(见 Bootstrap 5 docs)
- 若坚持要悬停,必须手动补 JS 逻辑,且需兼顾键盘焦点(
Tab)、ESC 关闭、移动设备降级等 - 纯 CSS 方案(如
:hover+display: block)在子菜单嵌套时容易因 mouseout 间隙导致菜单闪退
用原生 JS 实现带防抖和焦点支持的悬停下拉
推荐在 document.addEventListener('DOMContentLoaded', ...) 中初始化,避免 DOM 未就绪。核心是监听 mouseenter / mouseleave,并用 setTimeout 加防抖延迟关闭,同时监听 focusin 保证键盘可用。
const dropdowns = document.querySelectorAll('.dropdown-hover');
dropdowns.forEach(dropdown => {
const toggle = dropdown.querySelector('[data-bs-toggle="dropdown"]');
const menu = dropdown.querySelector('.dropdown-menu');
let timeoutId;
const show = () => {
clearTimeout(timeoutId);
bootstrap.Dropdown.getInstance(dropdown)?.show();
};
const hide = () => {
timeoutId = setTimeout(() => {
bootstrap.Dropdown.getInstance(dropdown)?.hide();
}, 150); // 鼠标移出后 150ms 再隐藏,防误触
};
toggle.addEventListener('mouseenter', show);
dropdown.addEventListener('mouseleave', hide);
dropdown.addEventListener('focusin', show);
dropdown.addEventListener('focusout', hide);
});
- 必须给外层
.dropdown添加自定义 class(如.dropdown-hover),避免影响其他纯点击下拉 - 依赖
bootstrap.Dropdown.getInstance(),确保已加载 Bootstrap 5 的 JS(不是仅用 CSS) -
focusout触发hide前需确认焦点是否真的离开了整个下拉区域,否则 Tab 切换子菜单项时会意外收起 - 移动端 Safari 等浏览器对
mouseenter支持不稳定,建议用touchstart+setTimeout做兼容 fallback
二级菜单(Submenu)结构与 CSS 微调
Bootstrap 5 不提供原生二级菜单组件,需手动扩展 HTML 结构,并用 CSS 控制定位和显示。关键点是让二级菜单相对一级菜单项定位,而非整个 .dropdown-menu 容器。
<div class="dropdown dropdown-hover">
<a href="#" class="dropdown-toggle" data-bs-toggle="dropdown">一级菜单</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">普通项</a></li>
<li class="dropdown-submenu">
<a class="dropdown-item dropdown-toggle" href="#">二级入口</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">子项 1</a></li>
</ul>
</li>
</ul>
</div>
- 二级菜单
<ul>必须放在<li class="dropdown-submenu">内,不能直接兄弟于一级<ul> - 需要额外 CSS:给
.dropdown-submenu .dropdown-menu设置position: absolute; left: 100%; top: 0;,并加margin-top: -2px对齐边框 - 悬停逻辑需递归绑定到
.dropdown-submenu内部的.dropdown-toggle,否则二级菜单无法悬停展开 - 注意 z-index 层级:二级菜单默认可能被父级
.dropdown-menu的overflow: hidden截断,需设overflow: visible或移除该样式
移动端适配和可访问性绕不开的坑
强行加 hover 后,最常被忽略的是「手机点一次展开、再点一次才进链接」这个交互断裂。用户点开二级菜单后,想点子项却意外关闭了整个下拉——因为第一次 tap 触发了 hover show,第二次 tap 才真正触发 a 标签跳转,但中间可能被 focus/blur 逻辑干扰。
- 检测
'ontouchstart' in window或matchMedia('(hover: none)').matches,对触屏设备禁用 hover 逻辑,回归 click 触发 - 为
.dropdown-item添加role="button"和tabindex="0",确保键盘用户能聚焦并回车触发 - ESC 键需关闭所有层级:监听
keydown,当event.key === 'Escape'时调用bootstrap.Dropdown.getInstance(el)?.hide()逐级向上收起 - 屏幕阅读器用户依赖
aria-expanded和aria-haspopup,JS 中需同步更新这些属性(例如toggle.setAttribute('aria-expanded', 'true'))
悬停式下拉看着方便,实则把一堆兼容性债堆在了 JS 层。真要上线,务必在 iOS、Android、Chrome+NVDA、Safari+VoiceOver 上各跑一遍焦点流和手势路径。不然上线后用户说「点不开」「关不掉」「读不出来」,问题全在你加的那几行 mouseenter 里。


















