:focus-within比:focus更适合导航菜单展开,因其在父容器或任意后代获得焦点时均保持激活,避免子菜单闪退;但需给父容器加tabindex="0",且仅safari 15.4+稳定支持。

直接结论:用 :focus-within 替代 :focus 控制下拉显隐,是目前最轻量、无需 JS 就能保证键盘用户从触发器一路操作到子菜单链接的方案;但必须给父容器加 tabindex="0",且 Safari 15.4+ 才稳定支持。
为什么:focus-within比:focus更适合导航菜单展开
因为 :focus 只在元素自身获得焦点时生效,而导航菜单的典型流程是:Tab 到「产品」按钮 → 点击或回车展开子菜单 → Tab 进入子菜单里的「产品A」链接。一旦焦点离开按钮进入子项,.nav-item:focus .submenu 立刻失效,菜单闪退——用户根本点不到链接。
:focus-within 解决了这个断层:只要 .nav-item 自身或其任意后代(比如子菜单里的 <a></a> 或 <button></button>)有焦点,它就持续匹配。这意味着:
- 焦点从触发按钮移入子菜单后,父容器仍保持激活状态
- 用户可连续按 Tab 遍历所有子项,无需鼠标干预
- 按 Shift+Tab 返回时,只要焦点没完全离开整个
.nav-item结构,菜单仍可见
必须加 tabindex="0",否则:focus-within不触发
很多开发者写了 .nav-item:focus-within .submenu { display: block; } 却无效,根本原因是:<div class="nav-item"> 或 <code><li class="nav-item"> 默认不可聚焦,浏览器永远不会给它分配焦点,:focus-within 自然无从谈起。
正确做法是显式声明可聚焦性:
- 给导航项容器加上
tabindex="0"(不是-1,否则无法通过 Tab 进入) - 确保子菜单里的所有交互元素(
<a></a>、<button></button>)本身可聚焦(它们默认就是) - 避免在父容器上设
disabled或display: none,否则 Safari 会跳过聚焦逻辑
:focus-within 和 :hover 共存时的样式冲突怎么避
鼠标悬停要展开,键盘聚焦也要展开,但二者行为逻辑不同::hover 鼠标移出即消失,:focus-within 要等焦点彻底离开容器才收起。如果合并写成 .nav-item:hover .submenu, .nav-item:focus-within .submenu { ... },某些浏览器会因解析优先级问题导致焦点状态下样式被 hover 规则覆盖或重置。
更稳妥的做法是分开声明,并让 :focus-within 后置以确保覆盖:
.nav-item:hover .submenu { opacity: 1; }
.nav-item:focus-within .submenu { opacity: 1; }
同时注意:
- 不要依赖
display: none → block做过渡动画(CSS 不支持 display 动画),改用opacity+visibility+transition - 移动端 Safari 对触摸后的焦点判定更严格,仅加
tabindex="0"不够,需配合onclick或touchstart触发一次focus()(JS 回退必备) - 旧版 Safari(iOS 15.3 及更早)完全忽略
:focus-within,必须用 JS 检测并降级为事件监听
容易被忽略的兼容性与降级细节
当前(2026年9月)主流浏览器已普遍支持 :focus-within,但仍有几个硬伤点必须手动处理:
- iOS Safari 15.4+、macOS Monterey+ 才稳定支持;旧版本会静默忽略规则,需用
@supports not selector(:focus-within)包裹降级样式 - 焦点进入子菜单后,若用户点击空白处或切换窗口,焦点可能被意外移走,导致菜单收起——这不是 CSS 能控的,需 JS 监听
blur并做防抖延迟隐藏 - 屏幕阅读器对
:focus-within的播报不一致,建议额外加aria-expanded属性并用 JS 同步更新 - 多层嵌套菜单中,外层
:focus-within会因内层子菜单的tabindex干扰而提前失焦,应限制子菜单容器不设tabindex,只让可操作子项(如<a></a>)承载焦点











