键盘可访问导航菜单:如何设置Tab键仅聚焦顶级菜单项
键盘可访问导航菜单实现方案
核心思路靠两个机制配合就能实现要求的交互:一是通过tabindex属性控制元素是否进入Tab默认焦点序列,二是绑定自定义键盘事件接管方向键的焦点移动逻辑。
核心实现规则
- 默认状态下,所有子菜单内的可交互链接统一设置
tabindex="-1",这类元素不会被Tab键遍历到,只有顶级导航链接保持tabindex="0",纳入默认Tab焦点流。 - 给顶级导航项绑定键盘监听事件:当焦点停在顶级项、用户按下向下方向键时,展开对应子菜单,把子菜单第一个链接的
tabindex改为0,同时直接将焦点移到该链接上。 - 补充子菜单内的键盘交互适配:子菜单内按上下方向键可在同组子项间循环切换焦点,按向上方向键到第一个子项时可回退焦点到顶级链接,按ESC键收起子菜单并将焦点切回对应顶级项。
- 焦点或点击事件触发到导航区域外时,自动收起所有展开的子菜单,将所有子菜单项的
tabindex重置为-1,避免下次Tab导航出现异常焦点跳转。
代码示例
HTML结构
<nav class="main-nav"> <ul class="nav-list"> <li class="nav-item has-submenu"> <a href="/products" class="nav-top-link" tabindex="0" aria-expanded="false">产品中心</a> <ul class="submenu" role="menu"> <li role="none"><a href="/products/laptop" role="menuitem" class="submenu-link" tabindex="-1">笔记本电脑</a></li> <li role="none"><a href="/products/phone" role="menuitem" class="submenu-link" tabindex="-1">手机</a></li> <li role="none"><a href="/products/tablet" role="menuitem" class="submenu-link" tabindex="-1">平板</a></li> </ul> </li> <li class="nav-item has-submenu"> <a href="/support" class="nav-top-link" tabindex="0" aria-expanded="false">技术支持</a> <ul class="submenu" role="menu"> <li role="none"><a href="/support/faq" role="menuitem" class="submenu-link" tabindex="-1">常见问题</a></li> <li role="none"><a href="/support/contact" role="menuitem" class="submenu-link" tabindex="-1">联系我们</a></li> </ul> </li> <li class="nav-item"> <a href="/about" class="nav-top-link" tabindex="0">关于我们</a> </li> </ul> </nav>
核心JS逻辑
const topLinks = document.querySelectorAll('.nav-top-link'); // 重置所有子菜单状态 function resetAllSubmenus() { document.querySelectorAll('.submenu').forEach(menu => { menu.classList.remove('is-open'); const parentLink = menu.previousElementSibling; if (parentLink) parentLink.setAttribute('aria-expanded', 'false'); menu.querySelectorAll('.submenu-link').forEach(link => { link.tabIndex = -1; }); }); } // 绑定子菜单内部键盘事件 function bindSubmenuKeyEvent(submenu, parentTopLink) { const subLinks = Array.from(submenu.querySelectorAll('.submenu-link')); submenu.addEventListener('keydown', (e) => { const currentIndex = subLinks.indexOf(document.activeElement); switch(e.key) { case 'ArrowDown': e.preventDefault(); const nextIndex = (currentIndex + 1) % subLinks.length; subLinks.forEach(link => link.tabIndex = -1); subLinks[nextIndex].tabIndex = 0; subLinks[nextIndex].focus(); break; case 'ArrowUp': e.preventDefault(); if (currentIndex === 0) { resetAllSubmenus(); parentTopLink.focus(); return; } const prevIndex = currentIndex - 1; subLinks.forEach(link => link.tabIndex = -1); subLinks[prevIndex].tabIndex = 0; subLinks[prevIndex].focus(); break; case 'Escape': e.preventDefault(); resetAllSubmenus(); parentTopLink.focus(); break; } }); } // 顶级项键盘事件绑定 topLinks.forEach(link => { link.addEventListener('keydown', (e) => { const submenu = link.nextElementSibling; if (e.key === 'ArrowDown' && submenu?.classList.contains('submenu')) { e.preventDefault(); resetAllSubmenus(); submenu.classList.add('is-open'); link.setAttribute('aria-expanded', 'true'); const firstSubLink = submenu.querySelector('.submenu-link'); firstSubLink.tabIndex = 0; firstSubLink.focus(); bindSubmenuKeyEvent(submenu, link); } }); }); // 点击导航外部时重置菜单 document.addEventListener('click', (e) => { if (!e.target.closest('.main-nav')) resetAllSubmenus(); });
注意事项
- 不要仅靠
display: none控制子菜单隐藏来隔离焦点,部分辅助技术仍可能识别到隐藏的可交互元素,必须配合tabindex="-1"才能真正把子项移出默认Tab序列。 - 顶级链接如果配置了实际跳转地址,不要拦截回车、空格键的默认触发事件,保证基础跳转功能正常。
- 子菜单展开/收起时同步更新
aria-expanded等无障碍属性,保证屏幕阅读器能正确识别菜单状态。
内容的提问来源于stack exchange,提问作者melissa
相关产品推荐
相关产品推荐

