纯 CSS 的 :hover 在触摸屏上不可靠,必须用 :focus-within + data-open + 媒体查询分层控制;需为每级菜单项设 position: relative 和 tabindex="0",子菜单须为直接子元素,并用 visibility/opacity/pointer-events 替代 display 控制显隐。

纯 CSS 的 :hover 在触摸屏上根本不可靠,这不是你代码写错了,而是浏览器主动降级行为——必须用 :focus-within + data-open + 媒体查询分层控制,缺一不可。
为什么 :hover 在 iOS/Android 上点一次没反应、点两次才跳转
这不是 bug,是规范行为:触摸设备没有“悬停”概念,第一次点击被浏览器当作模拟 hover(仅 Safari 会延迟约 300ms 触发,且只限首次),第二次才执行链接。依赖 :hover 控制显隐的菜单,在手机上本质是失效的。
-
@media (hover: hover)才启用:hover > .submenu,@media (hover: none)下该规则自动不生效 - 别用
@media (max-width: 768px)判断——Galaxy Z Fold 横屏超 1000px 却无 hover,1366×768 笔记本却支持 fine pointer - 所有
:hover规则必须成对出现:.has-submenu:hover > .submenu和.has-submenu:focus-within > .submenu
如何让键盘用户和屏幕阅读器也能操作多级菜单
:focus-within 是 WCAG 2.1 AA 合规的硬性要求,但只加在第一级不够——二级菜单项若无法获得焦点,键盘用户 Tab 进去就跳走了。
- 每一级
<li class="has-submenu">都要能聚焦:给它加tabindex="0" - 子菜单
<ul class="submenu">必须是其直接子元素,否则:focus-within不触发 - 禁用
display: none:它会让屏幕阅读器跳过整个结构;改用visibility: hidden+opacity: 0+pointer-events: none - 初始状态必须设为:
.submenu { visibility: hidden; opacity: 0; pointer-events: none; transition: all 0.25s ease; }
移动端点击展开必须用 data-open,而不是 JS 操作 DOM 显隐
CSS 本身没有 :click,但你可以用极简 JS 切换属性,把交互逻辑和样式彻底解耦。
立即学习“前端免费学习笔记(深入)”;
- JS 只做一件事:
el.dataset.open = el.dataset.open === "true" ? "false" : "true" - CSS 控制效果:
[data-open="true"] .submenu { visibility: visible; opacity: 1; pointer-events: all; } - 绝对禁止在媒体查询里写
.submenu { display: block !important; }——它会覆盖data-open,导致无法收起 - 不要监听
touchstart,改用click:iOS Safari 对 click 事件的兼容性更稳
定位错乱、菜单飞走、被裁切的三个关键修复点
多级菜单消失或错位,90% 是定位上下文和层叠控制出问题,跟动画参数关系不大。
- 每一级
<li>都必须显式设position: relative;Flex 容器不创建定位上下文 - 一级菜单子项用
top: 100%,二级起统一用left: 100%,别写死left: 0或top: 0 - 父容器(如
<nav>、<header>)禁用overflow: hidden——它会直接裁掉弹出的子菜单 -
z-index只设在外层<nav>上(如z-index: 100),子菜单靠层叠上下文自然堆叠,别每级都设不同值
真正难的不是写多少行 CSS,而是每级 <li> 是否都带 position: relative、是否都加了 tabindex="0"、是否所有 .submenu 都严格作为直接子元素存在——漏掉任意一个,菜单就在某类设备上“失联”。


















