自定义按钮必须设tabindex="0"并监听enter/space键,用aria-label提供语义,进度条须用input[type="range"]并动态更新aria-valuetext,状态变更需通过aria-live推送。

按钮必须有 tabindex="0" 且响应 Enter 和 Space
键盘用户无法用 Tab 键聚焦到自定义按钮,是可访问性第一道坎。原生 <button></button> 默认可聚焦、可按空格/回车触发,但很多开发者用 <div> 或 <code><span></span> 模拟按钮,又忘了加 tabindex="0",结果屏幕阅读器读不到、键盘也切不过去。
必须显式设置 tabindex="0",并监听 keydown 事件:
playBtn.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
video.paused ? video.play() : video.pause();
}
});
- 不依赖
click事件——它只响应鼠标或触摸,不覆盖键盘操作 - 必须调用
e.preventDefault(),否则按空格会触发页面滚动 - 别只监听
Enter:空格键是按钮的标准激活键,WCAG 明确要求
aria-label 不能省,title 和图标都不够
纯图标按钮(比如 ?、⏸️)对屏幕阅读器是“无意义字符”,title 属性在多数屏幕阅读器中默认不朗读,仅靠视觉暗示等于放弃视障用户。
每个控制按钮都应带明确语义的 aria-label:
<button aria-label="播放视频">▶</button> <button aria-label="静音">?</button> <button aria-label="切换全屏模式">⛶</button>
-
aria-label优先级高于title,且被所有主流读屏器支持 - 避免模糊表述,如“控制音量”——应写“降低音量”或“音量设为 30%”(如果支持精确调节)
- 播放/暂停按钮需动态更新
aria-label,不能只写死一个值
进度条必须用 input[type="range"],禁用 div + mousedown 手动实现
手写拖拽逻辑(监听 mousedown/mousemove)会导致进度条完全不可键盘操作,也无法被读屏器识别为“可调节控件”。WCAG 要求滑块类控件必须是语义化表单元素。
正确做法是用原生 <input type="range">,并补全属性:
<input type="range" min="0" max="100" step="1" aria-label="视频播放进度" aria-valuetext="已播放 23 秒,共 187 秒">
-
aria-valuetext必须动态更新,反映当前时间与总时长(如 “已播放 45 秒,共 210 秒”) - 不要用
change事件监听拖动结束——它延迟高;改用input事件实时响应 - 移动端需额外处理
touchstart/touchmove,但 DOM 元素本身仍是input,语义不变
播放状态变更要同步 aria-live 区域
当视频自动暂停(如网络中断)、播放完成或静音开启时,仅靠视觉提示(比如按钮变灰)对听觉/视障用户无效。需要主动推送状态变化。
在 DOM 中添加一个隐藏的实时区域:
<div aria-live="polite" aria-atomic="true" class="sr-only"></div>
然后在 JS 中更新内容:
const statusEl = document.querySelector('[aria-live]');
video.addEventListener('pause', () => {
statusEl.textContent = '视频已暂停';
});
video.addEventListener('ended', () => {
statusEl.textContent = '播放已完成';
});
-
aria-live="polite"确保状态变更不打断当前朗读,但会被及时读出 -
aria-atomic="true"防止读屏器只读变化部分,而是整句重读 - 别依赖 CSS 的
display: none隐藏该区域——要用sr-only类(clip + absolute 定位),否则读屏器可能忽略
aria-label 动态更新和 aria-valuetext 实时计算上——这两处一旦漏掉,整个控件就退回不可访问状态。











