多级目录语义化html结构须用包裹,子菜单为嵌套且是父的直接子元素,配合role="menu"/"menuitem"、aria-haspopup与aria-expanded属性确保可访问性。

多级目录的语义化HTML结构怎么写才不踩坑
直接用嵌套 <ul></ul> 做多级菜单,看似简单,但容易导致屏幕阅读器跳过子项、键盘焦点无法进入、SEO 权重稀释。关键不是“能不能嵌套”,而是“嵌套是否可访问、是否可控制”。
- 必须用
<nav></nav>包裹整个导航区域,而非<div class="nav"> <li>主菜单项用 <code><li role="none">,避免被读作“列表项 1 of 10”干扰节奏 - 每个可展开的父项加
aria-haspopup="true"和aria-expanded="false",初始状态要同步 - 子菜单容器用
<ul role="menu"></ul>,子项用<li role="none">+<a role="menuitem"></a>,不能只靠 CSS 隐藏/显示 - 子菜单默认
display: none,不要用visibility: hidden或opacity: 0单独控制——它们仍占流式空间或可被聚焦 - 展开时用
display: block+position: absolute,并设top: 100%对齐父项底部 - 过渡动画只作用于
opacity和transform,禁用height过渡(无法从auto动画) - 移动端需额外加
@media (hover: none) and (pointer: coarse)关闭 hover 效果,防止误触 - 用
el.toggleAttribute('aria-expanded')一行同步属性值和布尔状态 - 监听
click时,先event.preventDefault(),再对目标父项调用 toggle,避免锚点跳转 - 键盘支持必须处理
Enter和Space键,两者行为要完全一致 - 收起其他已开菜单时,别用
querySelectorAll('.open'),改用document.querySelectorAll('[aria-expanded="true"]')—— 更准确,不依赖 class 名 - 媒体查询切换时,不能只靠
display: none隐藏子菜单——要同时移除tabindex="-1"或设置inert(注意 Safari 旧版不支持) - 汉堡菜单展开后,主菜单应加
inert,或遍历所有非导航元素设tabindex="-1",防止焦点逃逸 - 子菜单关闭时,焦点必须返回触发它的父
<a></a>,而不是丢给document.body - 使用
window.matchMedia监听断点变化,而不是只在加载时判断一次
CSS 控制多级展开时,哪些属性组合最可靠
纯 CSS 实现下拉(不用 JS)是可行的,但仅限 hover 触发且依赖 :focus-within 或 :hover。移动端必须补 JS,否则点击无响应。重点不是“有没有动画”,而是“状态是否可控、是否可逆、是否阻塞滚动”。
JavaScript 控制展开收起时,为什么 toggleAttribute 比 classList.toggle 更合适
因为 ARIA 状态必须与 DOM 属性严格一致,而 classList.toggle 只改 class,不改 aria-expanded。浏览器不会自动同步 class 变化和 ARIA 属性,手动维护极易遗漏。
响应式断点下隐藏/折叠多级目录,最易忽略的兼容性问题
很多方案在桌面端切到移动端后,菜单收起但焦点还卡在已隐藏的子项里,用户按 Tab 就会“消失”在空白处。这不是样式问题,是焦点管理缺失。
多级目录交互真正的复杂点不在“怎么展开”,而在“谁有焦点、谁该被读、谁不该被 tab 到、状态如何跨设备同步”。这些细节一旦漏掉,用户可能根本点不开第二级,或者点了却不知道自己在哪一级。











