必须用语义结构,禁用div模拟;子菜单需嵌套并同步控制aria-expanded、焦点与动画状态,确保键盘导航闭环。

nav + ul + li 结构必须用语义化标签打底
不套 <nav></nav>、硬塞 <div class="nav">,后续加交互时会踩一堆坑:比如屏幕阅读器无法识别导航区域,<code>aria-expanded 属性失效,甚至某些浏览器的键盘焦点逻辑错乱。结构必须是 <nav><ul><li><a></a></li></ul></nav> 嵌套,子菜单用嵌套 <ul class="submenu"></ul>,不能靠 JS 动态拼 HTML 字符串来生成主干结构。
常见错误现象:
– 点击汉堡图标后菜单不展开,但控制台无报错
– 键盘 Tab 切换跳过整个导航区域
– 移动端双击缩放后菜单项错位
- 每个顶级
<li>必须包含一个<a></a>(哪怕 href="#"),否则focus()行为异常 - 有下拉的菜单项需加
aria-haspopup="true"和aria-expanded="false" - 子菜单
<ul></ul>需紧跟在对应<li>内,不能用绝对定位“飞出去”再塞进 DOM
toggleClass 不够用,得手动控制 aria-expanded 和 focus 状态
只写 element.classList.toggle("open") 是懒办法。CSS 里用 .submenu.open { display: block; } 看似简单,但可访问性直接归零——屏幕阅读器根本不知道这个菜单“现在打开了”。必须同步操作 ARIA 属性和焦点链。
使用场景:移动端点击汉堡图标、桌面端 hover + keyboard 触发下拉、ESC 键关闭所有展开项
立即学习“Java免费学习笔记(深入)”;
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- 点击父级
<a></a>时,先event.preventDefault(),再切换aria-expanded值 - 展开后,用
submenu.querySelector("a").focus()把焦点移到第一个子链接 - 按 ESC 键时,遍历所有
[aria-expanded="true"]元素,设为"false"并收起对应.submenu - 不要依赖
:hover实现下拉,它在触摸设备上无效;hover 只能作为桌面增强,核心逻辑必须由 JS 控制
响应式断点要和 JS resize 监听解耦
很多人把 window.addEventListener("resize", ...) 当成万能响应方案,结果一拖拽窗口就卡顿,或者小屏下误触发桌面逻辑。真实项目里,JS 不该“感知尺寸”,而应“响应状态”。
性能影响:频繁 resize 触发重排,尤其在 Chrome DevTools 模拟设备时更明显
- 用 CSS 媒体查询控制
.navbar的display或visibility,JS 只监听matchMedia("(max-width: 768px)").matches的变化 - 把移动端菜单开关逻辑封装成独立函数,如
initMobileNav(),只在媒体查询匹配时调用一次 - 避免在 resize 回调里反复 addEventListener,容易造成事件重复绑定
- 移动端菜单打开后,记得给
加overflow: hidden,防止背景滚动
动画和 transition 要配 JS 的 class 切换节奏
CSS 里写 .submenu { transition: max-height 0.3s ease; } 看起来平滑,但 max-height 从 0→具体像素值时,如果高度不确定(比如子项动态加载),动画会抽搐或提前结束。JS 控制动画节奏比纯 CSS 更稳。
容易踩的坑:
– 子菜单展开后内容被截断
– 关闭动画没执行完就再次点击,状态错乱
– Safari 下 transform: scaleY() 动画闪白
- 优先用
transform: scaleY(0) → scaleY(1)+opacity,避免触发布局计算 - 展开前先
submenu.style.height = "0",强制重绘,再加类触发 transition - 用
getComputedStyle(submenu).height获取最终高度后,再设回"auto",确保动画结束后样式干净 - 关闭动画完成后再移除
.open类,别让 JS 和 CSS transition “抢状态”


















