侧边栏抽屉键盘可访问性需同时解决结构、状态与JS接管问题:必须用aria-hidden+inert控制语义可见性,Escape关闭时准确回焦触发按钮,显式接管方向键与Tab循环,并确保焦点始终落在用户预期位置。

侧边栏抽屉打开后,键盘用户无法进入、按 Escape 关不掉、焦点卡在背景里——这些问题不是“加个 tabindex 就完事”,而是结构、状态、JS 接管三者同时出错的结果。
抽屉容器必须用 aria-hidden + inert 控制语义可见性
仅靠 display: none 或 visibility: hidden 隐藏抽屉,屏幕阅读器仍可能读取内容;只用 opacity: 0 或 transform: translateX(-100%) 则完全不阻断语义流。
- 抽屉关闭时,必须设
aria-hidden="true",并配合inert属性(现代浏览器支持)或手动移除所有子元素的tabindex和role - 抽屉打开时,
aria-hidden="false"不够,得同步移除inert,且确保内部第一个可聚焦项已存在(比如首项是<button>或带tabindex="-1"的<div role="button">) - 别把抽屉嵌套在
<main>或布局组件内:DOM 层级过深会导致inert传播失效,也影响document.activeElement判断
Escape 关闭必须触发焦点回退,且不能依赖 blur()
按下 Escape 后,抽屉收起,但焦点若留在已 aria-hidden="true" 的元素上,或直接丢失,键盘用户就“掉进黑洞”了。
- 关闭前,必须提前记录触发按钮(如
data-drawer-trigger="sidebar"),不能靠document.activeElement临时抓取——它可能已被其他逻辑覆盖 - 关闭动画结束(非 CSS transitionend,而是 JS 确认 DOM 已更新)后,再调用
triggerEl.focus({ preventScroll: true }) -
preventScroll: true在 Safari 不生效,需检测:if ('preventScroll' in FocusOptions.prototype),否则 fallback 到triggerEl.focus()并手动window.scrollTo() - 禁止对抽屉容器本身调用
blur():它不解决焦点去向,反而让document.activeElement变成body,后续回退无从下手
方向键导航和 Tab 循环必须由 JS 显式接管
抽屉里有长菜单、分组标题、禁用项?光靠 tabindex="0" 和原生 Tab 键,用户会反复跳过不可操作项,或直接跳出抽屉。
立即学习“前端免费学习笔记(深入)”;
- 监听
keydown,对ArrowDown/ArrowUp做处理;Home/End直接跳首/末项,不要等用户连按 - Tab 键离开抽屉时,用
focusin检测目标是否仍在抽屉容器内:!sidebarContainer.contains(event.target),成立则立即event.preventDefault()并聚焦到对应边界项 - 禁用菜单项(如
aria-disabled="true")不能设tabindex="0",否则仍会被 Tab 到;应设tabindex="-1"并移除role,或保留role="menuitem"但加aria-disabled="true"+tabindex="-1" - 所有可聚焦项必须有
:focus-visible样式,全局* { outline: none }必须被覆盖,否则键盘用户彻底失焦
移动端滚动穿透和焦点捕获失败常因 touch-action 缺失
iOS Safari 上,手指在抽屉菜单滑动却触发背后页面滚动,本质是事件冒泡未被拦截;焦点无法落到菜单项,则多因 position: fixed 容器未正确隔离输入流。
- 抽屉容器必须设
touch-action: pan-y,禁止横向拖拽干扰;内部可滚动区域(如菜单列表)单独设overflow-y: auto+touch-action: pan-y - 抽屉打开时,给
<body>加class="drawer-open",CSS 中写.drawer-open { overflow: hidden; }—— 比 JS 拦截touchmove更稳定,也不影响遮罩层点击 - 首次打开后,焦点必须落在第一个可聚焦元素(如首项
<button>或<div role="menuitem" tabindex="-1">),且该元素不能被display: none或visibility: hidden包裹,否则focus()静默失败 - 遮罩层(
#sidebar-overlay)必须与抽屉同级、同为<body>子节点,否则focus()无法穿透父容器的overflow: hidden或 flex 约束
真正难的不是让抽屉“能被键盘访问”,而是当它打开、滚动、关闭、重开时,焦点始终落在用户逻辑预期的位置——这要求每个环节都放弃“默认行为幻想”,用明确的 DOM 状态、克制的 tabindex、精准的 JS 聚焦时机来闭环。漏掉任意一环,键盘用户就会在黑暗中摸索几秒。



















