必须使用原生 的 indeterminate 属性实现半选状态,因其能同时触发正确视觉样式、屏幕阅读器播报及语义化交互;单独设置 aria-checked="mixed" 无效,除非配合完整 aria 协议与状态同步逻辑。

aria-checked="mixed" 不能单独设置生效,它必须配合 indeterminate 属性和正确的 DOM 状态逻辑一起用 —— 否则屏幕阅读器会忽略或误读。
为什么直接写 aria-checked="mixed" 没用
原生 <input type="checkbox"> 不支持通过 HTML 属性设置半选状态;aria-checked 是 ARIA 角色属性,只在元素有明确 role(如 role="checkbox")时才被辅助技术识别。但如果你用 <div> 自定义复选框,光加 <code>aria-checked="mixed" 而不处理焦点、键盘交互、状态同步,屏幕阅读器只会报“未选中”或直接跳过。
- 浏览器对
aria-checked的解析依赖于元素是否具有可交互语义(比如tabindex、role="checkbox"、aria-label或关联的<label></label>) - 即使写了
aria-checked="mixed",若没同时控制视觉样式(比如用 CSS:not([aria-checked="true"]):not([aria-checked="false"])::before),用户也看不到半选反馈 - React/Vue 等框架中,直接在 JSX/模板里硬写
aria-checked="mixed"会导致每次 re-render 覆盖状态,必须用 ref 或 state 精确控制
indeterminate 是唯一可靠触发半选视觉+语义的方式
只有原生 <input type="checkbox"> 支持 indeterminate 属性,且它是 JavaScript 可写、CSS 可响应的真状态。所有“半选”行为都该基于它构建:
- 设置
el.indeterminate = true时,el.checked必须为false(这是关键:不要设成true,否则视觉上显示为全选) - CSS 中可用
input:indeterminate伪类定制半选样式,比如background-image: url("dash.svg") - 屏幕阅读器在遇到
indeterminate为true的原生 checkbox 时,自动播报“部分选中”,无需额外aria-checked - 如果非要用自定义容器(如
<span></span>),就得手动实现全套 ARIA 协议:设role="checkbox"、监听Space键、同步aria-checked值、管理aria-labelledby,成本远高于直接用原生 input
监听子项 change 时别触发父项的重复计算
常见错误是:父 checkbox 点击后批量设置子项 checked,结果每个子项 change 又触发一次父项状态更新,导致 indeterminate 被反复覆盖。
- 更新子项时用
el.checked = value,但不要触发事件:可临时移除监听器,或用el.dispatchEvent(new Event('change', {bubbles: false}))控制传播 - 父项自己的 click 事件里,先清空子项监听,再批量赋值,最后恢复监听 —— 或更简单:用事件委托监听 tbody,避免给每个子项绑 handler
- 判断半选状态时,别用
[...children].some(c => c.checked) && [...children].some(c => !c.checked),而要用计数:const checkedCount = [...children].filter(c => c.checked).length,避免因 DOM 异步更新导致误判
真正要让半选“被看见、被理解、被操作”,核心不是怎么写 aria-checked,而是坚持用原生 <input type="checkbox"> + indeterminate + 语义化 <label></label>。所有绕过它的方案,最终都会在键盘导航、屏幕阅读器兼容或 React/Vue 的 re-render 边界上出问题。











