一个自动播放的轮播图:每 3 秒自己翻一页。想让它停下------焦点落在「下一张」上,按 Enter 只是再翻一页;松手,它继续走。页面上根本没有暂停键,切换时也没有任何播报。
读屏用户更被动:内容被换掉了,没有任何播报。这篇把「自动播放」当成一个可访问性问题来修,而不是一个动画参数。
先量一遍:它到底能不能停
对照页用 1 秒间隔(把 3 秒压缩,方便数),4 屏、带箭头和圆点。0.2.25 的实测:
| 指标 | 0.2.25 |
|---|---|
| 暂停控件 | 0 个 |
| 第一个 Tab 停靠点 | 上一张箭头 |
| 焦点在箭头上等 3 秒 | 索引 3 → 2(继续走) |
aria-live |
无 |
aria-roledescription |
无 |
指示点 aria-current |
0 个 |
| 系统「减少动态效果」下 | 仍然自动翻页 |
焦点在轮播内部、用户正在操作,它照样每 3 秒换一次内容。WCAG 2.2.2 的要求很直接:自动更新、且持续时间超过 5 秒的内容,必须给用户一个暂停、停止或隐藏的机制。箭头和圆点解决的是「手动切换」,不是「停下来」。
这类问题在 demo 里看不出来:画面一直在动,看起来「很正常」。只有把焦点放进去、数 3 秒,才会发现没有任何东西能让它停。
怎么量的
同一份 4 屏轮播,分别加载 npm 上的 0.2.25 与本地构建的 0.2.26,间隔压到 1 秒。状态行每 120ms 打印:索引、aria-live、暂停按钮与文案、aria-pressed、aria-current 数量、当前焦点、matchMedia 是否命中。
每组操作先读状态、断言符合预期,再截图。两版跑同一套 14 条断言:0.2.25 组按「预期坏」断言,0.2.26 组按「应该好」断言,全部通过。
源码:定时器从挂载跑到卸载
原来的自动播放只有两个函数:
ts
function startAutoplay() {
if (props.autoplay <= 0 || timer !== null) return;
timer = setInterval(() => { ... next(); }, props.autoplay);
}
onMounted(startAutoplay);
onBeforeUnmount(stopAutoplay);
没有「暂停」状态,没有系统偏好判断,也没有页面可见性判断。setInterval 从挂载一直跑到卸载,唯一的停止时机是拖动时临时停一下、松手再启动。
prefers-reduced-motion 也管不到它:那个媒体查询影响的是 CSS 过渡,而翻页是 JS 定时器驱动的。减少动效只关掉动画,不关掉计时器------这是第一个反直觉点。
修法一:暂停按钮必须是键盘第一站
先加一个真实的 <button>,aria-pressed 表达状态,文案在「暂停自动播放 / 继续自动播放」之间切换:
html
<button class="rn-carousel__pause" :aria-pressed="userPaused"
:aria-label="userPaused ? t('carousel.play') : t('carousel.pause')">
关键在 DOM 顺序:它排在箭头和圆点之前。这样按一次 Tab 就落在暂停键上,而不是先经过「上一张」「下一张」「第 1 张」......才找到它。
实测第一个 Tab 停靠点:0.2.25 是 rn-carousel__arrow--prev,0.2.26 是 rn-carousel__pause。
按钮默认 opacity: 0,悬停、键盘聚焦或已暂停时出现。不可见但仍可聚焦,是这里刻意保留的:鼠标用户看到的是干净的画面,键盘用户 Tab 过来时按钮会显形并获得焦点环。
为什么默认透明
一直显示会破坏画面;完全隐藏又找不到。折中是 opacity: 0 加三种显形条件:悬停、:focus-within、已暂停。键盘 Tab 到它时 :focus-within 生效,按钮出现并获得焦点环;已暂停时常驻,提醒用户「现在可以继续」。透明不等于不可点,但鼠标用户只在需要时看到它。
为什么不做「hover 自动暂停」
常见做法是鼠标移入就暂停。问题是它只对鼠标成立:键盘用户把焦点放在轮播里,定时器照走;触屏用户根本没有 hover。它改善体验,但不构成 WCAG 要求的「机制」。显式按钮才是。
为什么不做「focus 自动暂停」
另一个常见做法是焦点进入就暂停。实测会遇到一个死结:用户点「继续自动播放」时,焦点还在这个按钮上,focus 暂停逻辑立刻又把播放停掉------按钮看起来失灵。
显式按钮没有这个问题:暂停与否只由 userPaused 决定,与焦点无关。
修法二:aria-live 要跟着播放状态切换
轮播的内容变化要不要播报?答案取决于它是否在自动播放:
- 播放中 :
aria-live="off"。否则每 3 秒打断一次读屏,用户什么都听不完; - 暂停后 :
aria-live="polite"。此时翻页由用户触发,播报是必要的反馈。
html
<div class="rn-carousel__track" :aria-live="isAutoPlaying ? 'off' : 'polite'">
实测:0.2.25 的 track 没有 aria-live;0.2.26 播放中是 off,点暂停后变 polite。
这里我一开始想反了------以为「有 live region 总比没有好」。真实规则是「自动变化时闭嘴,用户操作时说话」,常开反而更吵。轮播里如果嵌了视频或动画,同样的切换逻辑也适用,判断依据始终是「这次变化是谁触发的」。
修法三:轮播本身的语义
<div> 包几屏内容,读屏软件只知道这里有一堆文字。补三样:
html
<div role="region" aria-roledescription="carousel" :aria-label="t('carousel.label')">
role="region":成为一个可跳转的地标;aria-roledescription="carousel":读屏会念「轮播」而不是「区域」;aria-label:给它一个名字(新增 locale 键,中文「轮播图」,英文 Carousel)。
指示点补 aria-current="true",读屏能知道「当前是第几张」。实测 aria-current 命中的点数:0 → 1。
修法四:减少动效与后台标签页
两处收尾:
- 挂载时读
matchMedia("(prefers-reduced-motion: reduce)"),命中就默认暂停,按钮显示「继续自动播放」。用户仍可手动继续------尊重偏好,但不剥夺选择。 - 监听
visibilitychange:页面隐藏时clearInterval,回到前台再续。后台标签页里每 3 秒翻一次页没有意义,还白耗电。
实测「减少动态效果」模拟(CDP 强制 reduce)等 2.6 秒:0.2.25 索引走到 2 ,0.2.26 停在 0,按钮文案是「继续自动播放」。
我以为是 A,实测是 B
第一处 :我以为「有箭头、有圆点」就等于可操作。实测键盘第一站是上一张箭头,按 Enter 只翻一页;等 3 秒,索引从 3 走到 2------没有任何控件能让它停。可操作不等于可停止。
第二处 :我以为 aria-live 加上就好。实测常开会每 3 秒打断读屏;正确做法是播放中 off、暂停后 polite。同一个属性,两种状态。
第三处:我以为「焦点进入就暂停」是最稳的方案。实测点「继续」时焦点还在按钮上,会立刻被自己的 focus 逻辑停掉,按钮看起来坏了。显式按钮把「用户意图」和「焦点位置」解耦,才没有这个死结。
第四处 :我以为 prefers-reduced-motion 能顺带关掉自动播放。实测它只影响 CSS 过渡,setInterval 完全不受影响;必须单独读 matchMedia 并停表。
换成另一种实现会怎样
| 方案 | 能停住吗 | 问题 |
|---|---|---|
| 只有箭头/圆点(原方案) | 不能 | 只能手动翻,不能停止自动更新 |
| hover 暂停 | 鼠标能,键盘不能 | 不构成 WCAG 机制;触屏无 hover |
| focus 暂停 | 键盘能 | 点「继续」时焦点仍在按钮,立刻又被停 |
| 显式暂停按钮(现方案) | 所有输入方式都能 | 多一个控件,需要处理与自动播放的状态同步 |
| 干脆不自动播放 | 不需要停 | 失去「自动展示」的产品诉求 |
第三行和第四行的差别,本质是「用焦点位置推断意图」和「让用户显式表达意图」。前者在组合交互里总有边界情况,后者没有。
边界:单屏、不循环、autoplay=0
- 只有一屏时定时器仍会启动,但每次 tick 直接 return------白跑;可以在启动前判断
slideCount <= 1就不建表。 loop=false走到末屏后停表,按钮文案仍是「暂停自动播放」但计时器已不存在;再播放需要先回到前面的屏。autoplay=0不渲染暂停按钮------没有自动播放,就没有暂停语义。- 拖动期间停表,松手后按
isAutoPlaying恢复;用户已暂停时拖动不会偷偷启动。
这些边界都不需要新 API,但每一个都可能让「暂停」在某种状态下名存实亡。
结果
0.2.26 已发布:暂停/继续按钮(DOM 第一站)、aria-live 双态、轮播语义与 aria-current、减少动效默认暂停、页面隐藏停表。
- 单测 923 → 926(暂停语义、Tab 停靠点与 aria-current、减少动效,共三条用例)
- 真机断言 14/14 通过:暂停后 3 秒索引 3→3;继续后恢复推进;0.2.25 对照组 3 秒内 3→2
- 文档站新增的示例可直接看到右下角的暂停键













结论
自动播放不是「加个 setInterval」那么简单,它是一个持续占用用户注意力的行为。三条规则,按重要性排序:
- 必须有一个显式的暂停机制,且要能被键盘第一时间够到------不是 hover,也不是靠焦点推断;
aria-live跟着播放状态走 :播放中off,暂停后polite;prefers-reduced-motion要单独判断,它管不到 JS 定时器;页面隐藏时也要停表。
最容易被忽略的是第 1 条里的「第一时间」:暂停键放在箭头和圆点后面,键盘用户仍要按 4 次 Tab 才能停------功能有了,但没解决问题。第 3 条也常被跳过:prefers-reduced-motion 看起来像个 CSS 开关,实际管不到 JS 计时器,必须单独读一次 matchMedia 再决定要不要建表。
组件解剖 · 系列目录
- Table:虚拟滚动的三个硬约束
- Radio / RadioGroup:ARIA 与方向键选中
- Transfer:三态复选框的三个 bug
- DatePicker:42 格日期网格的四个边界
- Select:可搜索下拉的键盘、ARIA 与滚动
- Carousel(本篇):自动播放的暂停机制与播报时机
在线示例:ui.ruanno.top/components/... 源码:gitee.com/ruanno-top/...