J5.10.3accessibility state synchronization设计研究

状态变化(展开、选中、禁用)需要实时同步到无障碍树而非仅改变外观

别名: 状态同步 · aria-expanded · aria-checked · aria-disabled

概念解释

箭头转了 90 度、背景变成灰色、对勾画出来了——这些都还在绘制层。辅助技术要知道「展开了、禁用了、选中了」,必须把对应属性写进无障碍树:状态同步(accessibility state synchronization)。aria-expandedaria-checkedaria-disabledaria-pressedaria-selected 以及原生元素自己的 disabled / checked,才是树上的状态。只改 class 和 CSS,树仍停在上一拍。

这不是「页面换了一块内容要喊一声」。那是动态播报的事。这里问的是同一个控件,它此刻处于哪一种状态,树有没有跟上外观。

机制

阅读器在焦点落到控件上、或状态属性变化时,把状态读出来:「已展开」「未选中」「灰色,不可用」。它读的是树节点上的属性,不读旋转角度。视觉层用过渡动画表示折叠,树如果仍是 expanded=true,语音会在动画结束后继续说展开。反过来,脚本把 aria-expanded 改了却没改画面,看见的人和听见的人各执一词。

第二层是时机。状态必须在用户动作的同一拍写入树,而不是等动画 transitionend。阅读器对属性变化的订阅是事件,不是每帧采样;晚一拍,用户已经按了下一次键,听到的是旧状态,下一次操作就建在错误前提上。禁用尤其容易只做「看起来灰」:指针事件被 CSS 挡住了,树上仍是可聚焦、可激活。

怎么研究

写一个手风琴或自定义复选框,分三档:只改外观;只改 ARIA;外观与 ARIA 一起改。每次切换后立刻让 NVDA / VoiceOver 读当前控件,并在无障碍检查器里看状态属性。再加一档:状态写在动画结束回调里,测用户连按两次时听到什么。

自变量:状态写入树的时机(同步 / 动画后 / 不写)、原生元素 vs 自定义。 因变量:语音中的状态词、检查器属性、第二次按键是否作用在用户以为的状态上。

边界

原生 <details><input type="checkbox"> 的开合与勾选由用户代理写入树,作者不必也不该再抄一份 aria-expanded。只读展示(静态的「已完成」徽章)若不是控件,没有「状态变化」这回事,不该被做成假的 aria-pressed。忙闲、加载这类瞬态更靠近播报,而不是控件状态;硬塞进 aria-busy 而不移开焦点,用户可能永远听不到。远程协作里「别人选中了」的状态,更新频率会把同步变成噪音,需要节流,但不能节流到用户自己的那一次点击。

怎么落地

  • 每次折叠、勾选、禁用、切换,在改外观的同一函数里改树上的状态属性,不要放进动画回调。
  • 禁用用真正的 disabledaria-disabled 并移出 Tab 序列或明确宣告不可用;不要只加灰色 class。
  • 自定义开关同时维护 aria-checkedaria-pressed 与可见的开合,两处用同一份数据。
  • 验证:关掉显示器,对每个可切换控件操作一次,听状态词是否翻转;再打开检查器看属性是否在点击当下就变。画面动了、属性没动,或属性动了、语音仍说旧状态,就是同步只做了一半。

延伸

  • 同组J5.10.1 可访问名称按固定优先级从多个来源计算得出,顺序可被开发者覆盖 · J5.10.2 视觉可见的文字与实际计算出的可访问名称可能不一致 · J5.10.4 自定义组件常见的失误是提供了角色却遗漏了对应状态属性
  • 相邻J5.02 无障碍树与角色 · J5.12 动态内容的播报
  • 站内检索accessibility state synchronization · aria-expanded · aria-checked

同组卡片

快捷操作

分享

分享当前页面

ios_share

https://hci.top/zh/handbook/J5.10.3