想象你蒙上眼睛,走进一座陌生博物馆。讲解员挽着你的手臂轻声说:“左手边是青铜器展厅,面前展柜里是一只商代酒杯,杯口有一道裂痕。”你看不见任何展品,却能靠他的描述在脑中拼出画面。对很多视障用户来说,屏幕阅读器就是这位“讲解员”——它把屏幕上看得见的布局,变成耳朵里听得见的讲述。可如果博物馆没有门牌、展品没有标签、讲解员又跟不上新展品的动向,参观就会变成一场灾难。网页同理:结构混乱、标识缺失、动态内容无人播报,读屏用户就会在数字世界里迷路。本文把 AI 产品比作一座无障碍博物馆,从结构、标识、指引、播报四个环节讲透屏幕阅读器适配,让你的产品即使“闭着眼”也能被顺畅听完。

岗位关联

学习屏幕阅读器适配,能帮助 AI 从业者把对话机器人、流式生成工具真正做成“人人可用”的产品,满足无障碍法规要求,扩大用户覆盖面,也体现 AI 产品的社会责任。

目录
  1. 可视化演示
  2. 读屏器工作原理
  3. 语义化HTML标签
  4. ARIA属性体系
  5. 焦点管理规范
  6. 动态内容播报
  7. AI产品适配实践
  8. 测试与验收工具
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示键盘Tab焦点流转、ARIA live区域动态播报及语义化HTML无障碍树结构,支持模式切换。

本节配有交互动效(进入视口后自动加载,约 3KB)。

键盘Tab焦点导航流程 红色高亮为当前焦点元素,按Tab键依次移动 每个可聚焦元素需有可见的focus指示器 tabindex=0 → 元素可被Tab访问 焦点管理确保键盘用户和屏幕阅读器用户能访问所有交互元素

读屏器工作原理

博物馆讲解员上岗前,先要研读场馆档案:入口在哪、展品怎么分布、动线怎么走。他从不在意墙纸的花纹,只依据登记在册的信息组织解说词。屏幕阅读器就是网页世界的这位讲解员——它“看”不见漂亮的视觉设计,只读取一份叫做“辅助功能树”的结构档案,然后按顺序讲述出来。

浏览器渲染网页时,除了画出像素画面,还会同步构建一棵精简的辅助功能树(Accessibility Tree)。树上每个节点记录三类信息:角色——这是按钮还是标题;可访问名称——它叫什么;状态——它是展开还是禁用。屏幕阅读器,像 macOS 的 VoiceOver、Windows 的 NVDA、Android 的 TalkBack,正是靠这棵树来“讲解”。用户并不使用鼠标,而是用 Tab 键、方向键或手势移动一个抽象的“读屏焦点”。

因为朗读基于树而不是画面,读屏顺序默认跟随 DOM 顺序——代码里先写什么,就先念什么。视觉上“看起来在右边”的元素,代码里可能排在左边元素之后,读屏顺序就会与视觉直觉相反。所以适配的第一课是:不要让视觉布局与 DOM 顺序脱节,重要内容要保持在读屏路径的前端。

讲解员讲得好不好,取决于档案整理得对不对;读屏体验顺不顺,取决于辅助功能树清不清晰。对 AI 产品来说,这份“档案”往往由前端框架和生成式代码共同决定,从源头整理好它,是一切适配工作的起点。

a11yNode(E)=⟨ role(E), name(E), state(E) ⟩\displaystyle \text{a11yNode}(E) = \langle\, \text{role}(E),\ \text{name}(E),\ \text{state}(E) \,\rangle
公式含义:浏览器把一个网页元素 E 转化为辅助功能树上的一个三元组——角色、名称、状态,屏幕阅读器朗读的就是这三项。
  1. role 角色告诉读屏软件“这是什么”,例如按钮、标题或链接。原生 <button> 自动获得 button 角色,无需额外声明。
    role(E)∈{button, heading, link, textbox, … }\text{role}(E) \in \{\text{button},\ \text{heading},\ \text{link},\ \text{textbox},\ \dots\}
  2. name 可访问名称元素的“名字”,也就是用户听到的朗读内容。比如一个链接的可访问名称通常是它包含的文字。
    name(E)=textContent(E)\text{name}(E) = \text{textContent}(E)
  3. state 状态元素的当前状态,如展开、选中、禁用,是动态交互信息的重要来源。
    state(E)∈{expanded, checked, disabled, … }\text{state}(E) \in \{\text{expanded},\ \text{checked},\ \text{disabled},\ \dots\}

语义化HTML标签

走进一座正规博物馆,你看到的是:展厅门口挂着“青铜器馆”的门牌,展柜旁贴着说明牌,岔路口立着导览图。这套固定标识系统让讲解员能快速定位、组织路线。网页的语义化 HTML 标签,就是给数字博物馆装的“门牌”。

<h1> 到 <h6> 是楼层指示牌,标明内容的层级;<nav>、<main>、<aside> 是功能分区的地标;<ul> 和 <ol> 是带数量的展品清单。读屏用户不需要从第一行听到最后一行——他们可以按标题跳转、按地标导航,就像游客直接看路牌而不是游遍全馆。

具体规范包括:标题层级要连续,一个页面最好只有一个 h1,不能从 h3 突然跳到 h1;按钮、链接、输入框必须用原生元素,因为 <div onclick> 既没有角色,也不会响应回车键;表单控件要用 <label for> 与输入框关联,读屏才能念出“用户名:请输入文字”这样的完整提示;列表用原生 <ul>/<ol>,读屏会播报“共 5 项”。

门牌挂对了,讲解员才能流利地带队参观。AI 生成页面时,如果模板里都是没有语义的 <div> 堆砌,读屏用户听到的将是一锅没有结构的“糊糊”。生成式代码必须默认输出语义化标签,这是 AI 前端工程的一条铁律。

role(E)={navigationE=⟨nav⟩mainE=⟨main⟩buttonE=⟨button⟩textboxE=⟨input⟩\displaystyle \text{role}(E) =\begin{cases}\text{navigation} & E = \langle \text{nav} \rangle \\[4pt]\text{main} & E = \langle \text{main} \rangle \\[4pt]\text{button} & E = \langle \text{button} \rangle \\[4pt]\text{textbox} & E = \langle \text{input} \rangle\end{cases}
公式说明:原生语义标签会隐式声明对应的 ARIA 角色,读屏软件无需任何猜测就能正确播报,这是“免费”的无障碍能力。
  1. 原生映射浏览器对每个语义标签赋予默认角色,开发者零成本获得无障碍支持。
    ⟨nav⟩↦role: navigation\langle \text{nav} \rangle \mapsto \text{role: navigation}
  2. 地标区域nav、main、aside 等标签构成 landmark,读屏用户可以用快捷键快速跳转。
    landmark(E)=role(E), if E∈{nav, main, aside}\text{landmark}(E) = \text{role}(E),\ \text{if } E \in \{\text{nav},\ \text{main},\ \text{aside}\}
  3. 交互控件button、input 等原生控件自动支持键盘聚焦与回车激活。
    interactive(E)  ⟺  role(E)∈{button, link, textbox}\text{interactive}(E) \iff \text{role}(E) \in \{\text{button},\ \text{link},\ \text{textbox}\}

ARIA属性体系

有些展品很特殊——互动屏幕、触摸模型,无法贴传统说明牌。博物馆会在旁边加一块盲文补充牌,把必要信息补上。ARIA 属性就是网页世界里的“盲文补充牌”,专门给那些没有原生语义、或语义表达不足的自定义组件补充信息。

ARIA(Accessible Rich Internet Applications)有三大件:role 声明“这是什么”,状态属性描述“现在怎么样”,标签属性给出“它叫什么”。但要记住第一法则:没有 ARIA 胜过糟糕的 ARIA。能用一个原生 <button> 解决的问题,就不要发明一个带 role="button" 的 <div>。

实践要点:自定义标签页组件声明 role="tablist" 和 role="tab";折叠面板用 aria-expanded 表达展开状态;开关按钮用 aria-pressed;没有文字的图标按钮用 aria-label="关闭" 提供名称;引用型名称用 aria-labelledby 指向页面上另一段文字,可实现动态更新。千万别改变原生元素的角色——<button role="link"> 会让读屏播报成链接,行为却还是按钮,用户会被严重误导。

盲文牌不能乱贴——贴错了反而把游客绊倒。ARIA 是“补充手段”而非“万能胶水”。在 AI 动态生成的界面里,模型容易产出语义不明的 div 堆砌,工程师要在生成模板中预设正确的角色与状态,让这块“盲文牌”从一开始就贴对位置。

name(E)←aria-labelledby(E)≻aria-label(E)≻textContent(E)≻title(E)\displaystyle \text{name}(E) \leftarrow \text{aria-labelledby}(E) \succ \text{aria-label}(E) \succ \text{textContent}(E) \succ \text{title}(E)
公式说明:可访问名称的多个来源同时存在时,取优先级最高的那个作为播报名称,从左到右依次降级,直到找到可用项。
  1. labelledby 优先被引用元素的文本优先作为名称,适合名称需要随状态动态变化的场景。
    aria-labelledby(E)=text(E′)where id(E′)=aria-labelledby(E)\text{aria-labelledby}(E) = \text{text}(E')\quad \text{where } id(E') = \text{aria-labelledby}(E)
  2. label 直接声明没有 labellbody 引用时,使用 aria-label 直接书写的字符串。
    aria-label(E)="自定义短标签字符串"\text{aria-label}(E) = \text{"自定义短标签字符串"}
  3. 内容与 title 回退仍无声明则退化为元素内部文字,最后才用 title 属性兜底。
    textContent(E)≻title(E)\text{textContent}(E) \succ \text{title}(E)

焦点管理规范

讲解员手里有一支激光笔。他讲到哪,笔尖就指到哪,游客的注意力随之移动。键盘焦点就是读屏用户的“激光笔”——它标示出当前正被操作的元素。焦点管理,就是如何优雅地控制这支笔,不让它在黑暗里乱晃,也不让它停在错误的位置。

鼠标用户点击即是聚焦;键盘用户用 Tab 在可聚焦元素之间移动,用 Enter 或空格激活。默认情况下,Tab 顺序等于 DOM 顺序。tabindex 可以微调:tabindex="0" 把元素加入 Tab 序列,tabindex="-1" 让元素可被脚本聚焦但不进入自然序列,正整数的 tabindex 会打乱自然顺序,除非万不得已不要使用。

三个硬性要求:其一,焦点必须有可见样式,用 :focus-visible 加清晰的描边或高亮,让键盘用户看得到“笔尖在哪”;其二,模态框必须做焦点圈定,打开时焦点移入框内,Tab 只能在其内部循环,关闭后焦点归还给触发元素;其三,页面发生动态变化(如 AI 回答完成)时,要把焦点主动移动到结果区域,而不是让用户停留在原来的位置不知所措。

激光笔绝不能把游客带进展厅后自己跑出去——焦点必须始终跟着用户走。对 AI 产品尤其如此:自动弹出的建议按钮、异步加载的答案、突然出现的错误提示,每一处都考验着工程师对焦点的掌控。管好这支笔,读屏用户才不会被扔进漆黑的界面角落。

next(i)=first+((i−first+1) mod n)\displaystyle \text{next}(i) = \text{first} + \left( (i - \text{first} + 1) \bmod n \right)
公式含义:模态框内共有 n 个可聚焦元素,从第 i 个按 Tab 时,下一个位置是(i-first+1)对 n 取模再加回 first,从而实现首尾相接的循环,防止焦点逃出弹窗。
  1. first 起点模态框内第一个可聚焦元素,作为循环计数的参照点。
    first=min⁡{f∣f∈F}\text{first} = \min\{f \mid f \in F\}
  2. 步进与回绕每次 Tab 索引加一,用取模 n 让最后一个元素回到第一个。
    d=(i−first+1) mod nd = (i - \text{first} + 1) \bmod n
  3. 映射回实际元素偏移量 d 对应可聚焦元素集合中的真实下标。
    next(i)=F[first+d]\text{next}(i) = F[\text{first} + d]

动态内容播报

讲解员正讲着青铜器,隔壁书画厅突然开门了。他压低声音,在句与句之间补了一句:“右侧新开了书画展,您有兴趣的话稍后可以去。”——不打断主讲解,又把消息送到。网页里也有一种“低声提醒”的机制,叫 live region,中文常称为无障碍直播区。

网页上动态插入的内容,屏幕阅读器默认不会主动播报——插入了就像什么都没发生。想让读屏“开口”,必须把容器标记为 live region。aria-live="polite" 表示等当前朗读结束再插话;aria-live="assertive" 表示立即打断;role="alert" 是系统级强提醒。

细节规范:aria-atomic="true" 让整个区域作为一句话播报,而不是只念变化的小片段;生成中的区域设 aria-busy="true",完成后置回 false,避免朗读中间状态;错误提示用 role="alert",即时抓起用户注意力;还要注意不要频繁改动 live region 的内容,否则读屏会连续朗读、互相打断,形成“话痨风暴”。

什么时候低声耳语、什么时候大声示警,讲解员心里有一本礼仪账。AI 产品里,加载回答用 polite 提示,流式生成要批量节流播报,系统报错要立即 alert。把播报节奏控制好,读屏用户听到的就是一位训练有素的“讲解员”,而不是一个喋喋不休的复读机。

P(live)={1role=alert2aria-live=assertive3aria-live=polite∞无 live 标记\displaystyle P(\text{live}) =\begin{cases}1 & \text{role} = \text{alert} \\[4pt]2 & \text{aria-live} = \text{assertive} \\[4pt]3 & \text{aria-live} = \text{polite} \\[4pt]\infty & \text{无 live 标记}\end{cases}
公式说明:数字越小播报越急切。alert 是最高优先级强提醒,assertive 次之,polite 最温和;完全没有 live 标记则内容永远不会被主动播报(视为无穷大,即不播报)。
  1. alert 立即打断系统级错误必须立刻让用户知道,不等当前朗读结束。
    role=alert⇒P=1\text{role} = \text{alert} \Rightarrow P = 1
  2. assertive 插队播报重要但可容忍打断的更新,插到当前播报之前。
    aria-live=assertive⇒P=2\text{aria-live} = \text{assertive} \Rightarrow P = 2
  3. polite 排队播报普通内容更新,等当前朗读完成后再播报。
    aria-live=polite⇒P=3\text{aria-live} = \text{polite} \Rightarrow P = 3
  4. 无标记不播报默认状态,动态插入的内容对读屏用户“隐身”。
    P=∞⇒不播报P = \infty \Rightarrow \text{不播报}

AI产品适配实践

有了讲解员、门牌和盲文牌,博物馆还专门设计了一条“语音导览专线”:每有新展品,讲解词都提前打磨。AI 产品也该如此——从设计第一天就把无障碍纳入动线规划,而不是上线后再打补丁。

AI 产品最常见的形态是对话式界面:用户提问,AI 流式输出答案。这类界面给读屏带来独特挑战:内容持续变化、新元素不断插入、长文本分多轮追加。适配目标可以概括为三件事:播报稳定——不重复、不遗漏、不碎读;焦点可控——输入框与回答区呼应,操作不被“粘住”;交互可键盘化——所有快捷操作都能脱离鼠标完成。

实践清单:流式回答放进 role="status" 的区域,按句子缓冲、节流写入,避免逐字蹦词;“正在思考”用 aria-busy="true" 配合 polite 提示,而不是反复更新文字;推荐问题列表用 <button> 渲染,天然支持 Tab 与回车;多轮对话只播报新回答,历史记录不重复朗读,可用 aria-relevant="additions" 控制变化类型。

语音导览专线之所以好听,是因为讲解词被编辑成“一句一句”,而不是“一字一字”。AI 工程师就是这篇讲解词的编辑:把模型吐出的 token 流,剪辑成读屏友好的完整句子,再配以合适的焦点指引,视障用户才能真正与 AI 产品自然对话。

announce(t)  ⟺  S(t)≥Ts ∨ (t−tlast)≥Tt\displaystyle \text{announce}(t) \iff S(t) \geq T_s \ \lor \ (t - t_{\text{last}}) \geq T_t
公式含义:当累积的文本片断长度 S(t) 达到长度阈值 T_s(如 40 字),或距离上次播报超过时间阈值 T_t(如 600 毫秒),就触发一次播报;两个条件任一满足即触发,保证既及时又不碎片化。
  1. 长度阈值缓冲区的字符数攒够了才播报,避免一个字一个字往外蹦。
    S(t)=∑k∣chunkk∣≥TsS(t) = \sum_{k} |\text{chunk}_k| \geq T_s
  2. 时间阈值即使字符数不够,也要保证间隔一段时间,防止播报长期饥饿。
    Δt=t−tlast≥Tt\Delta t = t - t_{\text{last}} \geq T_t
  3. 或逻辑触发两个条件任一满足,就更新一次 live region 的内容。
    cond1∨cond2⇒announce(t)\text{cond}_1 \lor \text{cond}_2 \Rightarrow \text{announce}(t)

测试与验收工具

博物馆开业前,馆长请来真实的视障游客走一遍全程动线,让实习生戴上全遮光眼罩“盲测”各个展厅,再用清单逐项核对盲文牌是否齐全。无障碍测试与验收,就是这套“三重保障”。

三层测试互有分工:自动化扫描工具(axe-core、Lighthouse、WAVE)能快速找出缺失的 alt、非法 ARIA 值、对比度不足等规则性问题,但只能覆盖约三成规则;人工读屏测试用 NVDA、VoiceOver、TalkBack 实操走查,才能评估“听起来顺不顺”;AI 辅助审计让大模型分析 DOM 与生成代码,输出改进行动建议,用于放大前两者的效率。

落地建议:把 axe 规则接入 CI,阻断性错误直接拦截合并请求;每周安排一次“键盘之旅”——关掉鼠标,用 Tab 和快捷键完成核心流程;再开读屏软件完整听一遍,重点检查 AI 回答的播报是否连贯、焦点是否合理;最后用大模型对页面做一轮预审,把常见问题批量标注出来。

盲测游客的一句“导航让我迷路了”,胜过十份自动化报告。屏幕阅读器适配的终审标准永远是真实用户能否听明白、走通畅。自动化与 AI 只是加速器,“人能不能用”才是无障碍那杆最硬的尺。

score=NpassNtotal×100%\displaystyle \text{score} = \frac{N_{\text{pass}}}{N_{\text{total}}} \times 100\%
公式说明:通过的无障碍检查项数除以总检查项数再乘以 100%,得到百分比分数。自动化规则、人工用例与 AI 审计建议合并计入总分。
  1. 检查项集合由自动化规则、人工用例与 AI 审计建议共同组成。
    Ntotal=∣Cauto∪Cmanual∪CAI∣N_{\text{total}} = |C_{\text{auto}} \cup C_{\text{manual}} \cup C_{\text{AI}}|
  2. 通过项统计统计满足验收要求的所有检查项。
    Npass=∣{c∈Ctotal∣pass(c)}∣N_{\text{pass}} = |\{c \in C_{\text{total}} \mid \text{pass}(c)\}|
  3. 转化百分比标准化为百分制,便于跨团队对齐与迭代追踪。
    score=NpassNtotal×100%\text{score} = \frac{N_{\text{pass}}}{N_{\text{total}}} \times 100\%

AI 应用场景

AI 客服助手的对话播报

阶段:交互实现与测试验收

解决的问题:传统客服页面收到新回复时整体刷新消息列表,读屏会将整个对话从头朗读,视障用户无法定位新消息;若用逐字写入,又是一场碎读灾难。AI 方案把消息容器变为 role="status" 的 polite live region,按句子缓冲、节流写入,同时把焦点转移到新消息,实现“只播新增”。

具体案例:某银行网站的 AI 理财助手改造后,VoiceOver 用户反馈“终于能听清完整回答”,多轮对话任务完成率提升近一倍。

AI 文案生成工具的流式输出

阶段:前端交互设计

解决的问题:大模型逐 token 输出长文案,若每个 token 都触发读屏,用户听到的是无法理解的碎片。AI 方案:设置累积缓冲区,达到句子结束符(句号、换行)或时间阈值才更新 live region,并用 aria-busy="true" 表示“生成中”。

具体案例:广告文案生成器接入句子级节流后,NVDA 用户从“完全不可用”变为“生成完后听完整句”,操作错误率显著下降。

大模型辅助无障碍审计

阶段:测试与质量保障

解决的问题:人工读屏测试耗时巨大,自动化工具又查不出语义层级问题。AI 方案:用大模型批量分析页面 DOM,识别标题跳级、按钮无名称、live 区域缺失等隐患,并输出修复建议与参考代码。

具体案例:某团队对 500 个页面做大模型预审,自动标注出 80% 的常见问题,人工复核后整体审计周期从三周缩短到两天。

Python 演示

python68 行
# -*- coding: utf-8 -*-"""Dynamic HTML accessibility pre-check for screen reader adaptation."""from html.parser import HTMLParser  class A11yChecker(HTMLParser):    def __init__(self):        super().__init__()        self.headings = []          # 收集 h1-h6 标签        self.live_count = 0         # aria-live 区域数量        self.issues = []            # 发现的无障碍问题        self._in_button = False        self._btn_parts = []     def handle_starttag(self, tag, attrs):        attr_map = dict(attrs)        if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):            self.headings.append(tag)        if tag == "button":            self._in_button = True            self._btn_parts = [attr_map.get("aria-label", "")]        if attr_map.get("aria-live"):            self.live_count += 1     def handle_data(self, data):        if self._in_button:            self._btn_parts.append(data.strip())     def handle_endtag(self, tag):        if tag == "button" and self._in_button:            self._in_button = False            if not "".join(self._btn_parts).strip():                self.issues.append("按钮缺少可访问名称(aria-label或文本)")  def analyze(html):    parser = A11yChecker()    parser.feed(html)    last_level = 0    for tag in parser.headings:        level = int(tag[1])        if last_level and level > last_level + 1:            parser.issues.append("标题跳级: {} 之前应补充 h{}".format(tag, last_level + 1))        last_level = level    return parser  def main():    html = """    <h1>AI 智能助手</h1>    <h3>快捷提问</h3>    <button>发送</button>    <div aria-live="polite" id="answer"></div>    """    parser = analyze(html)    print("标题结构:", parser.headings)    print("动态播报区数量:", parser.live_count)    print("发现问题:")    for issue in parser.issues:        print(" -", issue)    if not parser.issues:        print(" - 无")    score = max(0, 100 - len(parser.issues) * 25)    print("预检评分: {}".format(score))  if __name__ == "__main__":    main()
目的:解析 AI 产品生成的 HTML 片段,自动检测标题跳级、按钮缺名称、动态播报区缺失等屏幕阅读器适配问题,并输出预检评分。
  1. from html.parser import HTMLParser使用 Python 标准库解析 HTML 片段,无需第三方依赖,任何环境都能跑
  2. self.headings = []记录页面出现的所有标题标签,供后续跳级检测使用
  3. if tag in ("h1", "h2", ...)命中标题标签时追加记录,标题是读屏用户跳转导航的主干
  4. attr_map.get("aria-live")统计动态播报区数量,AI 流式输出场景必须有 live region
  5. if not "".join(self._btn_parts).strip()按钮如果既没有 aria-label 也没有可见文本,读屏将无法播报
  6. if last_level and level > last_level + 1检测标题跳级,h1 直接到 h3 会让读屏用户误以为漏听了内容
  7. score = max(0, 100 - len(parser.issues) * 25)每个问题扣 25 分,最低 0 分,用分数直观驱动修复
  8. if __name__ == "__main__":提供可执行入口,直接运行脚本即可看到预检报告

常见误区

误区我以为只要给自定义组件加上 role 和 aria-label,屏幕阅读器就能读懂所有内容,甚至比原生标签更好用。

正解ARIA 的第一法则是“没有 ARIA 胜过糟糕的 ARIA”。原生 HTML 标签自带角色、键盘交互和状态管理,ARIA 只负责“修补”,且不能与原生语义冲突。给 <button> 强行加 role="link" 会让读屏播报成链接,用户听到的跟实际行为完全对不上,比不加更糟。

为何常见:ARIA 看起来像“万能补丁”,贴上就有反应,初学者容易把它当成无障碍的捷径。

误区我觉得读屏软件就是把页面上所有文本从左到右、从上到下读一遍,所以只要文字在页面上就行。

正解屏幕阅读器读取的是辅助功能树,按 DOM 顺序和语义结构朗读;熟练用户会通过标题、列表、地标快速跳跃,就像游客看路牌而不是走遍全馆。没有语义的页面,即使文字都在,用户也“听”不出重点,导航效率极低。

为何常见:我们习惯用视觉理解页面,容易把“看到”当成“读到”,忽略了读屏的结构化特征。

误区现在都用鼠标和触屏,谁还用 Tab 键?焦点样式去掉也能接受。

正解大量视障、运动障碍用户完全依赖键盘和读屏,根本没有可见指针。焦点可见样式(如 focus-visible 描边)是 WCAG 2.4.7 的 AA 级要求,也是读屏用户定位“当前操作位置”的唯一视觉线索。去掉 Tab 顺序或隐藏焦点,等于没收了讲解员的激光笔。

为何常见:开发和测试都用鼠标,永远看不到焦点,于是默认它“不重要”。

误区AI 回答是动态生成的,我在结果容器上加 aria-live="polite",读屏就会自动念出完整答案。

正解live region 只是“播报许可证”,播报质量取决于内容写入方式。AI 流式输出如果逐字写入,读屏会一字一字念,用户根本听不连贯;必须按句子批处理、节流写入,配合 aria-busy 标记“正在生成中”,才能形成自然流畅的朗读。播报礼仪和技术细节同样重要。

为何常见:大家首先想到“通知读屏有变化”,却忽略了 AI 输出是高频碎片的特殊情况。

误区Lighthouse 无障碍分数 100,页面肯定没问题,不用再人工测试了。

正解自动化工具只能覆盖约三成规则,主要检测“可机械判断”的问题(缺 alt、对比度、非法属性值)。“朗读是否连贯”“焦点顺序是否合理”“动态播报是否及时”这类体验问题,必须靠键盘走查和真读屏软件实测才能发现。自动化是过滤器,不是终审官。

为何常见:自动化测试结果客观、可量化,容易让人误以为它覆盖了全部验收标准。

面试题

Q1. 屏幕阅读器主要依赖什么来读取网页内容?

单选题 · easy

  • A.渲染后的视觉像素
  • B.辅助功能树语义信息← 正确答案
  • C.网页截图的识别结果
  • D.浏览器样式表解析

答案:B。屏幕阅读器读取的是浏览器构建的辅助功能树(语义结构),而不是渲染后的像素。就像讲解员依据展品档案解说,而不是靠看墙纸颜色。选项 A/C 都依赖视觉信息,D 的样式表不包含语义,都不正确。

Q2. 语义化 HTML 标签的主要作用是什么?

单选题 · easy

  • A.让页面显示更加美观
  • B.提升搜索引擎排名
  • C.为读屏提供语义结构← 正确答案
  • D.加快页面加载速度

答案:C。语义化标签(如 nav、main、button)向辅助功能树提供角色信息,屏幕阅读器据此正确播报。A 属于 CSS 的职责,B 是附带效应而非核心目的,D 与加载性能无关。

Q3. “没有 ARIA 胜过糟糕的 ARIA”这句话的含义是?

单选题 · mid

  • A.禁止使用任何ARIA属性
  • B.优先使用原生语义标签← 正确答案
  • C.ARIA完全没有无障碍支持
  • D.只有英文页面能用ARIA

答案:B。这条第一法则指能用原生 HTML 语义表达时优先使用原生元素,只有原生表达不足时才补充 ARIA。A 是极端化误读,C/D 与事实不符。错误使用 ARIA 会误导读屏播报,比不用更危险。

Q4. 原生 <button> 相比 <div onclick> 的核心优势是?

单选题 · mid

  • A.自动获得键盘交互与语义← 正确答案
  • B.页面渲染速度更加快速
  • C.需要的代码量更少
  • D.视觉样式更容易定制

答案:A。原生 button 自带 button 角色、Tab 聚焦和 Enter/Space 激活,无需手工模拟键盘事件。B/C/D 是常见印象,但都不是核心差异。用 div 模拟按钮还要补 role、tabindex 和 keydown 监听,极易出错。

Q5. 模态框打开时,焦点应该如何处理?

单选题 · mid

  • A.留在打开前的触发元素上
  • B.移入模态框并循环圈定← 正确答案
  • C.自动移到页面最底部
  • D.移到浏览器地址栏上

答案:B。模态框打开时焦点必须移入框内,并让 Tab 在框内循环(焦点陷阱),关闭时归还给触发元素。A 会让焦点停在背景、读屏无法感知弹窗,C/D 会造成焦点丢失,用户不知身在何处。

Q6. aria-live 的 polite 与 assertive 值核心区别是?

单选题 · mid

  • A.礼貌模式安静时才插话播报← 正确答案
  • B.断言模式会降低播报音量
  • C.两种模式都会延迟播报
  • D.两种模式都立即强制播报

答案:A。polite 会在当前朗读结束后再播报,assertive 会立即打断当前播报。B 把 assertive 误解为音量控制,C/D 忽略了两者优先级差异。日常 AI 回答更新应选 polite,紧急错误用 role="alert"。

Q7. AI 流式输出适配屏幕阅读器的最佳方式是?

单选题 · hard

  • A.每生成一个文字块就立即播报
  • B.攒成完整句子分批节流播报← 正确答案
  • C.直接禁用读屏的播报功能
  • D.让用户轮询刷新整个页面

答案:B。AI 流式输出应按句子批处理并节流写入 live region。A 逐字写入会造成碎读;C 违反无障碍原则;D 轮询刷新会触发整页重读,效率极低。句子级节流才能兼顾及时性与连贯性。

Q8. 以下哪些属于焦点管理的范畴?

多选题 · mid

  • A.键盘Tab键的导航顺序← 正确答案
  • B.焦点可见样式与判定← 正确答案
  • C.模态框内焦点圈定← 正确答案
  • D.屏幕亮度与色彩调节

答案:ABC。焦点管理包括 Tab 顺序、可见样式和模态框焦点陷阱。D 的屏幕亮度调节属于系统显示设置,不属于网页焦点管理范畴。焦点管理是读屏用户“激光笔”的完整控制策略。

Q9. 关于可访问名称(accessible name)的来源,以下哪些是合法的?

多选题 · hard

  • A.由其他元素指向的文本← 正确答案
  • B.直接声明的简短标签← 正确答案
  • C.元素内含的可见文本← 正确答案
  • D.元素设置的背景颜色

答案:ABC。可访问名称按 aria-labelledby > aria-label > 元素文本内容 > title 的优先级计算。A 对应 labellbody,B 对应 label,C 对应文本内容。D 的背景颜色不参与名称计算,也没有无障碍语义。

Q10. 下列哪些是屏幕阅读器适配的有效测试手段?

多选题 · easy

  • A.只用键盘完整走通核心流程← 正确答案
  • B.使用屏幕阅读器实地测试← 正确答案
  • C.自动化规则扫描页面← 正确答案
  • D.人工比对像素级截图

答案:ABC。键盘走查、读屏实测、自动化扫描是三类互补的无障碍测试手段。D 的截图像素比对只能检查视觉效果,无法验证语义与播报质量。测试的终审标准是真实用户能否听明白、走通畅。