骨架屏所有节点必须显式设 aria-hidden="true",不可仅设外层;加载完成需用 aria-live 主动通知,且服务端直出时须同步写入该属性。

骨架屏必须显式设 aria-hidden="true",但不能只设在外层容器上
屏幕阅读器会把骨架屏里的每个空 div 当成真实内容逐个朗读,比如“空白、空白、空白”,尤其当骨架里混了 img 或伪元素文字时,可能触发“缺失 alt”警告。你不能指望读屏软件自动忽略它——必须主动声明。
常见错误是只在最外层加 aria-hidden="true",但旧版 NVDA 会忽略嵌套继承,导致子节点仍被读出。正确做法是:所有骨架节点(包括 div、span、button 等)都得单独带上该属性。
- 不要用
role="presentation"替代,语义不如aria-hidden="true"明确,兼容性也差 - 避免给骨架元素加
title或空alt,部分读屏器仍会播报“空标题” - 骨架容器本身不能有
tabindex="0"或focusable="true",否则键盘用户会卡在里面
加载完成必须靠 aria-live 主动通知,不能只靠 DOM 替换
骨架屏消失、真实内容出现,这个动作本身不会触发任何可访问性事件。屏幕阅读器不知道“现在有内容了”,用户可能还在等,或误以为加载失败。
你得手动往一个 aria-live 区域插入一句文案,比如“商品列表已加载完成”。这个区域必须独立存在,不能复用骨架容器(它已被 aria-hidden="true" 封锁,无法播报)。
- 推荐用
aria-live="polite"+aria-atomic="true",assertive只留给错误类强提醒 -
class="sr-only"必须含position: absolute; width: 1px; height: 1px;等样式,确保视觉隐藏但读屏可读 - 两次更新间隔至少 500ms,否则读屏器会跳过中间播报
服务端直出骨架时,aria-hidden 必须同步写入 HTML
很多团队 SSR 输出骨架 HTML,但忘了在模板层就带上 aria-hidden="true"。结果 JS 还没执行,读屏器已经把骨架结构全读了一遍——此时页面还没交互能力,用户却已听到一堆“空白”。
解决方案是把可访问性逻辑前移到服务端:骨架结构输出时,就带 aria-hidden="true" 和 data-loading="true";JS 加载完成后,移除 data-loading,并清空或改为 aria-hidden="false"(注意不是设为 false,而是移除该属性)。
- 不要等
DOMContentLoaded或mounted再补属性,太晚了 - 若用 Vue/React,服务端渲染的骨架节点需与客户端 hydration 结构完全一致,否则属性会被覆盖或丢失
-
aria-busy="true"可用于按钮等交互元素,但不能替代全局骨架的aria-hidden
为什么不能用 display: none 隐藏骨架后再显示真实内容
看似简单粗暴,但会导致 NVDA + Firefox 组合下真实内容不被朗读。原因是部分读屏器会缓存 aria-hidden 状态,即使你后来移除了该属性,它仍认为“这部分还是不可读”。
更稳妥的做法是让骨架始终保留在 DOM 中,仅用 visibility: hidden 或 opacity: 0 + pointer-events: none 控制显隐。这样语义流不断,切换后读屏器能自然感知到新内容。
- 用
replaceWith()替换骨架节点时,务必调用remove()清理原节点,否则残留 DOM 可能干扰焦点流 - 骨架和真实内容必须严格同构:class 名大小写、子元素数量、
data-属性名值都要一致,否则 hydration 错位会破坏可访问性链路 - 移动端高 DPR 下,用伪元素 +
transform做动画比background-position更清晰,也更少触发重排
aria-hidden 或忘记 aria-live 更新的风险就越高——这恰恰是多数团队在压测和 A/B 测试中完全忽略的一环。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











