checkbox / radio / select
全库里最容易做坏的一组。浏览器的键盘行为是无障碍的地基—— 改坏了用鼠标完全测不出来,只有键盘用户会知道。
checkbox
整个 label 都能点(input 透明铺满了它),但视觉方框只有 20px——触摸目标是 44px。
.choice__hint 是解释,不是标签的一部分:
它排在标签下方、和文字左对齐,并且用 aria-describedby 关联 ——
读屏念"订阅月度报告,复选框",再补一句说明。
(写进 label 里会变成名字的一部分,念起来像"订阅月度报告每月1日发一封可随时退订"。)
indeterminate(第三态)
"全选"框初始是半选(横线),因为下面只选了 2 个。 不做这个态,"全选"复选框就无法正确表达中间状态。
radio 组(fieldset + legend)
用 ↑↓ 在组内移动,
移动即选中——这是原生 radio 行为,<div> 模拟不出来。
fieldset + legend 让屏幕阅读器播报"第几组、共几项、当前选中第几项"。
换成 <div>+<span> 这些信息全丢。
select
用原生 select,不做自定义下拉。
移动端会弹出系统带 wheel 的原生选择器、语言跟随系统、无障碍由系统保证。
换成 <div> 模拟,这些全部要重写且必然漏一部分。
这里只改了外观(appearance:none + 自绘箭头),
↑↓ 选择、Enter 确认、Esc 关闭全部保留。
箭头写了 pointer-events:none,否则它会吃掉点击。
.select__control--auto:固定 44px 高度遇到换行文字会把第二行裁掉,
所以多选列表 / 长选项要把高度放开(height:auto + min-height:44px)。
这是原生 select,箭头就不需要了 —— 它本来就不是"点开下拉"的交互。
disabled
不用 opacity,用明确的低对比色组(对比度会变成不可控的随机数)。
disabled 之后 hover 不能有任何变化,否则看着像还能点。
对照:把原生勾选框藏起来的两种写法
❌ display:none:看不见,也 Tab 不到
按一次 Tab 就知道:焦点直接跳过它。
display:none 把这个 input 从 tab 顺序里删掉了,
键盘用户彻底到不了 —— 而鼠标点仍然有效,所以这个 bug 用鼠标永远测不出来。
✅ opacity:0 + 绝对定位:看不见,但还在 Tab 顺序里
本库的 input 是 opacity:0 铺满整个 label:
你看得见的是 .choice__mark,焦点环也画在它身上
(:focus-visible + .choice__mark)。
焦点环必须画在看得见的地方
input 是 opacity:0 的,所以它自己的 outline 你根本看不见。
焦点环必须画在紧邻的可见标记上:
.choice__input:focus-visible + .choice__mark
门禁也是这么查的——它只认 :focus-visible + 这种相邻写法,
单纯写 :focus-visible {} 判为不通过。