aria-busy="true"是告知屏幕阅读器“正在加载”的唯一语义信号,须加在动态更新的容器上并配合role="region"等语义角色,加载完成必须设为false,不可与aria-live混用。

怎么用 aria-busy 告诉屏幕阅读器“正在加载”
不能只靠视觉判断加载状态——对使用读屏软件的用户来说,aria-busy="true" 是唯一能明确传达“内容暂不可用、别急着读”的语义信号。它不触发任何样式或行为,纯属无障碍声明。
常见错误现象:aria-busy 加在容器上,但新内容插入后没及时设回 false;或者加在按钮上(无效),其实必须加在**正在被动态更新的容器元素**上(比如 <div id="list">)。
<ul>
<li>
<code>aria-busy="true" 必须配合 role="region" 或已有语义容器(如 main、section)使用,否则多数读屏器忽略
true 后,读屏器会暂停对该区域的自动朗读(包括焦点进入时的默认播报)aria-busy="false",否则该区域永久“失声”aria-live 混用——aria-live 是通知变化,aria-busy 是冻结通知,二者逻辑冲突无限滚动容器为什么必须设 role="feed" 或 role="list"
单纯用 div + overflow-y: auto 实现滚动,对读屏用户等于“一堵墙”。浏览器不会主动把新增列表项当作可导航结构,焦点也无法用方向键逐条移动。
使用场景:新闻流、评论区、商品瀑布流等持续追加内容的区域。
立即学习“前端免费学习笔记(深入)”;
- 优先用
role="feed"(HTML 5.2+ 推荐),它语义上就表示“时间序内容流”,读屏器支持原生导航(如 NVDA 的F键跳到下一条) - 若需兼容老版本读屏器(如 JAWS 2019 及更早),改用
role="list"+ 每个子项加role="listitem" - 避免用
role="scrollable"(非标准属性,无读屏支持) - 容器必须有明确的
id,且首次加载时就存在 DOM 中,不能等 JS 创建完才挂载
IntersectionObserver 触发加载时,怎么避免重复请求和错位渲染
无限滚动的核心是监听底部元素是否进入视口,但 IntersectionObserver 的回调时机和阈值设置不当,会导致“刚加载完又触发一次”或“滚动过快时漏掉触发”。
参数差异:threshold 设为 0(默认)时,只要 1px 进入就触发,容易抖动;设为 1 则必须完全可见才触发,可能卡住。
- 推荐
threshold: [0, 0.1, 0.5]—— 多级触发,提前预加载,避免白屏 - 每次触发后立刻调用
observer.unobserve(target),防止同一元素多次回调 - 加载中状态要同步控制:设
aria-busy="true"+ 禁用滚动监听(用布尔标志位),避免快速滚动时并发请求 - 新内容插入后,用
scrollIntoView({ block: 'nearest' })对齐旧末尾,否则滚动位置会跳变
为什么 tabindex="-1" 要加在每条新加载的列表项上
读屏用户常用 Tab 键线性浏览内容,但无限滚动中新插入的项默认不可聚焦。不加 tabindex="-1",这些项就彻底“隐身”于键盘导航路径之外。
容易踩的坑:只给首条加,或加在容器上(无效);更糟的是加 tabindex="0",导致 Tab 键顺序混乱、焦点跳转失控。
- 每条新插入的项(如
<article></article>或<li>)都必须带tabindex="-1" - 插入后立即执行
element.focus()是反模式——会强制打断用户当前操作;正确做法是让用户自己按 Tab 到达 - 如果该项本身含可交互元素(如按钮),则无需额外加
tabindex,由内部焦点管理即可 - 配合
role="feed"时,部分读屏器(如 VoiceOver)会自动将新项纳入“下一条”导航,但仍建议保留tabindex="-1"保底
最易被忽略的是:aria-busy 和 role 属性必须在 JS 插入新内容前就写死在容器上,而不是等加载完成再补——DOM 插入瞬间读屏器就开始解析语义,晚了就来不及。



















