tabindex不是开关而是焦点流微调器;lang缺失致voiceover用英文音调读中文;role与tabindex搭配错误比不加更糟——这才是移动端无障碍阅读卡住的真实节点。

tabindex 不是键盘导航的“开关”,而是焦点流的“微调器”;lang 缺失会让 VoiceOver 把整页中文读成英文音调;role 和 tabindex 搭配错误,比不加更糟——这些才是移动端无障碍阅读卡住的真实节点。
移动端屏幕阅读器依赖 DOM 顺序,不是视觉位置
绝对定位、transform 或 display: none 会把元素从可聚焦流中剔除,但 VoiceOver 仍可能读出它(尤其当 aria-hidden="false" 或没设 aria-hidden)。用户听到内容,却无法用双指滑动或 Tab 切换到对应控件,体验直接断裂。
- 避免用
position: absolute把按钮“挪”到视觉顶部,而 DOM 还在底部——屏幕阅读器按 DOM 读,键盘按 DOM 走 - 轮播图切换时,仅靠
transform: translateX()移动面板,必须同步更新aria-current="true"和当前项的tabindex="0"(若需键盘进入) - 模态框打开后,别只靠
opacity: 0或visibility: hidden隐藏背景,得用inert属性(Chrome 101+),或手动遍历所有交互元素并设tabindex="-1"
tabindex="0" 加在哪?只加在“本不该聚焦但业务要它进 Tab 流”的地方
原生 button、a、input 天然在 Tab 流里,加 tabindex="0" 反而干扰 SSR hydration 或 React 的焦点管理逻辑。真正该加的,是那些语义缺失但又必须被键盘用户操作的容器。
- 自定义折叠面板标题:
<h3 role="button" tabindex="0">订单详情</h3>—— 必须配role="button",否则读作“三级标题”,且要监听keydown响应Enter和Space <div role="tab"> 标签页头,需 <code>tabindex="0"+aria-selected+aria-controls- 纯图标按钮(如
<span aria-label="返回"></span>)不能只靠aria-label,得包裹成<button aria-label="返回"></button>,而不是给span加tabindex="0" -
必须写在根标签,不能只给某段文字加lang - 双语混排时,局部切语言要用
<span lang="en">API</span>,否则整段被统一音调处理 -
dir值只能是"ltr"、"rtl"或"auto";auto在混合文本中不可靠,建议显式声明 - 所有可交互元素强制加
min-width: 44px; min-height: 44px;,用padding扩展,别只调font-size - 禁用
outline: none后,必须写:focus-visible { outline: 2px solid #0066cc; },且颜色对比度 ≥ 4.5:1 - 禁用
user-select: none在按钮或链接上——TalkBack 会因此跳过该元素,无法触发长按菜单
lang 和 dir 是语音输出的底层开关
没设 lang="zh-CN" 的页面,VoiceOver 在 iOS 上默认用美式英语发音读中文字符,比如把“支付宝”念成 “Zhī Fù Bǎo”(拼音腔),甚至断句错乱;阿拉伯语或希伯来语页面缺 dir="rtl",数字和标点顺序全乱。
触控目标与焦点反馈必须同时达标
WCAG 2.2 明确要求最小触控区为 44px × 44px,但很多团队只加了 padding 扩展点击区,却忘了键盘用户需要看到焦点在哪——:focus 被 outline: none 干掉后,:focus-visible 又没补,结果手指能点,键盘却“盲走”。
lang 是否准确、DOM 顺序是否匹配语义流、以及 tabindex 是否只在必要处“破例”。这三个点一旦松动,后续所有增强都像在移动沙丘上搭积木。











