骨架屏需显式隐藏语义并主动播报状态,否则屏幕阅读器会误读空元素;禁用aria-hidden="true"于外层容器以防NVDA缓存不可访问状态;应使用visibility: hidden或opacity: 0 + pointer-events: none隐藏,配合aria-live通告加载完成。

骨架屏必须显式隐藏语义、主动播报状态,否则屏幕阅读器会把一堆空div当成内容朗读,甚至加载完成后仍沉默不报。
为什么 skeleton 元素不能加 aria-hidden="true"?
很多团队图省事,在最外层容器加 aria-hidden="true",以为“关掉就完事”。但旧版 NVDA(尤其搭配 Firefox)会缓存该状态,等真实内容替换进来、骨架被 display: none 移除后,读屏器仍认为“这一块区域不可访问”,直接跳过新内容。
真正安全的做法是:骨架元素始终保留在 DOM 中,仅用 visibility: hidden 或 opacity: 0 + pointer-events: none 视觉隐藏;同时确保它们不进入焦点流(不设 tabindex,不响应 focus)。
- 禁用
role="presentation"替代方案——语义模糊,部分读屏器支持不一致 - 所有骨架节点(包括子级
<div>)都不要带title、alt、aria-label,哪怕为空字符串,某些读屏器仍会播报“空标题” - 避免伪元素
::before/::after插入content: "loading"—— 会被误读为冗余文本
加载完成时怎么让屏幕阅读器“知道内容来了”?
骨架消失本身不是可访问事件。用户听不到任何提示,只能靠摸索或等待,体验断裂。你得用 aria-live 主动通告。
立即学习“前端免费学习笔记(深入)”;
在 HTML 中提前声明一个不可见但可播报的实时区域:
<div aria-live="polite" aria-atomic="true" class="sr-only"></div>
当真实内容渲染完毕,JS 往这个区域插入一句简短文案,例如:document.querySelector('[aria-live]').textContent = '商品列表已加载完成'。
- 别复用骨架容器做 live region —— 它已被
aria-hidden封锁,无法播报 - 两次更新间隔至少 500ms,否则读屏器会跳过中间播报
- 用
aria-live="polite"足够,assertive只留给错误或中断类通知 -
class="sr-only"必须含position: absolute; width: 1px; height: 1px;等规则,确保视觉隐藏但读屏器可读
服务端输出骨架时,可访问性属性必须同步注入
SSR 框架(如 Next.js、Nuxt)常在服务端直出骨架 HTML,但若没同步写入可访问性标记,JS 还没执行前,读屏器就已开始朗读一堆“空白、空白、空白”。
正确做法是服务端模板中骨架结构一生成,就带上:
-
aria-busy="true"(首屏骨架容器必须有) -
aria-hidden="true"—— 注意:这是针对纯视觉占位的明确声明,且需写在每个骨架节点上,不能只靠继承 -
data-loading="true"(供客户端 JS 后续移除状态)
客户端 JS 加载后,真实内容挂载完成,再移除 data-loading,并往 aria-live 区域写入完成文案。整个链路从服务端到客户端,可访问性状态必须连续、无断点。
最容易被忽略的是:骨架结构里混了 <img> 或 <svg>,哪怕只是占位,也会触发资源加载、解析阻塞,甚至被读屏器当作缺失 alt 的无效图像反复提示。纯 CSS 实现(background + linear-gradient + 尺寸控制)才是唯一可靠路径。



















