双层导航必须严格遵循语义化与可访问性规范:主菜单用包裹,次导航作为主菜单的直接子嵌套,禁用多余容器;CSS需设position: relative于父、position: absolute于子,并避免overflow: hidden裁切;键盘导航须支持Arrow键切换焦点、Escape关闭并回溯焦点,且动态加载时确保DOM插入后聚焦。

主导航用 <nav></nav> 包裹,次导航必须嵌套在语义化容器里
主菜单是全站一级入口,必须用 <nav></nav> 标签包裹,并加 aria-label="主菜单"。次导航(比如“产品子类”“用户中心二级项”)不能直接平铺在 里,否则屏幕阅读器会误判为并列层级。常见错误是写成两个独立的 <nav></nav>,结果被读作“菜单一、菜单二”,丢失主从关系。
正确做法是:次导航放在主菜单项内部,用 <div role="group"> 或 <code><ul class="submenu"></ul> 包裹,且仅在触发后才渲染(避免初始 DOM 过载)。若用 JS 控制显隐,记得同步切换 aria-expanded 和 aria-hidden。
- 主菜单项链接需带
aria-haspopup="true",激活时设aria-expanded="true" - 次导航容器必须设
role="menu",子项用role="menuitem",禁用<a></a>直接跳转(会打断键盘导航) - 不要给次导航加
display: none初始隐藏——要用visibility: hidden; position: absolute配合aria-hidden="true",否则 Safari 会提前读取
ul/li 结构必须严格嵌套,li 里不能塞 div 再包 ul
语义结构错位是双层菜单最常崩的点。很多开发者把次导航写成:
<li><div><ul><li>子项</li></ul></div></li>
这违反了 HTML5 对 <ul></ul> 的嵌套规则:<ul></ul> 只能直接子元素是 <li>,中间插 <div> 会导致部分浏览器解析失败,键盘 Tab 键跳过整个子菜单。<p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</a>”;</p>
<p>正确结构只允许一层嵌套:</p><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/xiazai/skill12116" title="DeepBI DHTML 页面生成器"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20d3191a227.jpg" alt="DeepBI DHTML 页面生成器" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/xiazai/skill12116" title="DeepBI DHTML 页面生成器">DeepBI DHTML 页面生成器</a>
<p>生成可在 deepbi 沙箱 iframe 内运行或作为独立文件运行的交互式 HTML 页面,使用 `window.deepbi` JS SDK 实现数据查询、空间管理、文件浏览及用户操作。当用户提出以下需求时启用该能力:
- 创建 KPI 仪表盘、数据表格、文件浏览器、成员列表、管理页面;
- 明确要求“转成 dhtml”、“转译成 HTML”、“把 XX 页面转成……”、“控制台页面”、“管理后台”、“DeepBI 页面迁移”;
- 提及位于 `apps/console/src/featur</p>
</div>
<a href="/xiazai/skill12116" title="DeepBI DHTML 页面生成器" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div>
<pre class="brush:php;toolbar:false;"><ul class="main-nav">
<li>
<a href="#" aria-haspopup="true">产品</a>
<ul class="submenu">
<li><a href="/product/a">A系列</a></li>
<li><a href="/product/b">B系列</a></li>
</ul>
</li>
</ul></pre>
<ul>
<li>次导航 <code><ul></ul> 必须是主菜单 <li> 的**直接子元素**
<li> 下最多一个 <ul></ul>,不能多个或混排 <p></p>、<span></span>
::before 伪元素,别加额外标签破坏层级CSS 控制显隐时,overflow 和 position 必须配对设
纯 CSS 实现下拉(比如 hover 展开),最容易漏的是容器溢出控制。次导航默认 position: absolute,但父级 <li> 若没设 position: relative,下拉项会相对于 定位,滚动时错位;更隐蔽的问题是:父容器若设了 overflow: hidden,下拉菜单直接被裁掉——尤其在 flex 布局的导航栏里高频发生。
- 主菜单
<li>必须设position: relative - 次导航
<ul></ul>设position: absolute; top: 100%; left: 0;,且min-width: 200px防窄屏挤压 - 整行导航容器(如
<header></header>)禁用overflow: hidden,改用clip-path或透明遮罩做视觉截断 - 移动端收起状态,用
max-height: 0; overflow: hidden配合 transition,别用height: 0—— 因为高度不固定时会失效
键盘导航必须支持 ArrowDown/Escape,不能只靠鼠标
双层菜单的焦点流一旦断裂,残障用户就卡死。仅靠 CSS :hover 或 JS click 绑定远远不够。测试时用键盘 Tab 进入主菜单项,按 ArrowDown 应立刻聚焦第一个子项,ArrowUp 返回上一个,Escape 关闭当前展开的次菜单并返回主菜单焦点。
- 监听
keydown事件,对<a></a>主项拦截ArrowDown,阻止默认跳转并手动.focus()到子菜单第一项 - 次菜单内用
ArrowUp/ArrowDown循环焦点,Tab退出到下一个主项(不是下一个子项) -
Escape触发时,不仅要blur()当前焦点,还要把焦点移回触发它的主菜单<a></a> - 所有交互操作后,检查
document.activeElement是否落在预期节点,避免焦点丢失到body
.focus() 无效。这点容易被忽略,尤其在 Vue/React 动态渲染场景下。


















