原生控件更可靠因其天然支持键盘聚焦、屏幕阅读器朗读及系统触控反馈,而自定义元素需手动实现且在部分 webview 中失效;拖放组件须用 js 重建交互路径并提供语义反馈;标签页需显式管理焦点迁移;移动端触摸目标应控制 dom 深度≤6 层并确保 48×48px 尺寸。

原生控件为什么比自定义元素更可靠
因为原生 <button></button>、<input type="checkbox">、<select></select> 天然支持键盘聚焦(Tab)、空格/回车触发、屏幕阅读器朗读、系统级触控反馈(如 iOS 的 VoiceOver 点击音效),而自定义 <div role="button"> 即使加了 <code>tabindex="0" 和 aria-label,在微信 X5 内核、部分 Android WebView 中仍可能被跳过或无响应。
常见错误现象包括:双指滑动导航时“按钮”直接消失、点击无视觉反馈但 JS 逻辑却执行了、焦点进入后按空格无反应。
- 原生控件自动继承
:active样式和click事件链,无需额外监听keydown - 自定义元素必须手动监听
keydown并区分Space和Enter,否则键盘用户无法操作 -
role="button"不改变 DOM 行为,也不触发浏览器默认的表单提交或滚动行为
如何让拖放组件支持键盘与屏幕阅读器
HTML 原生 draggable 属性对辅助技术完全不可见——它不暴露语义、不支持键盘、不提供状态反馈。真正可访问的拖放必须用 JavaScript 重建交互路径。
关键点不是“让它能拖”,而是“让用户知道它可拖、能选中、有目标、已放置”。
- 所有可拖拽项需设
tabindex="0",并用aria-grabbed="false"初始化 - 按空格键时切换
aria-grabbed="true",同时播报 “项目A已选中,等待放置” 到aria-live="polite"区域 - 可放置区域需设
aria-dropeffect="move"(或"copy"),并高亮边框+图标变化(不能只靠颜色) - 放下后立即更新
aria-grabbed="false",重置焦点到新位置,并播报结果
标签页组件的键盘与焦点管理陷阱
很多实现只加了 role="tablist" 和 aria-selected,却忽略了焦点不会自动跳转到内容区——用户按 Tab 后卡在 tab 按钮上,看不到对应面板,也读不到内容。
正确做法是:当用户用方向键切换 tab 或按 Enter 激活时,JS 必须显式调用 tabpanelElement.focus(),且该面板需设 tabindex="-1" 才能被聚焦。
- 方向键(←→)只切换 tab 按钮焦点,不激活;Enter/Space 才触发切换并移动焦点
- 隐藏的内容区不能用
display: none,应改用aria-hidden="true"+visibility: hidden,否则屏幕阅读器会跳过整个结构 - 首次加载时,确保默认选中的 tab 对应的 panel 已设
tabindex="-1"并获得焦点
移动端触摸目标与 DOM 深度的实际限制
48×48px 是最低安全尺寸,但若父容器嵌套过深(>6 层),即使尺寸达标,低端安卓设备上 touchstart 延迟可达 40ms 以上,导致点击“失灵”或重复触发。
这不是 CSS 问题,而是事件冒泡路径变长 + 中间层若有 pointer-events: none 或 touch-action: none,就可能中断手势链。
- 按钮结构尽量扁平:推荐
<button><span>文本</span></button>(2 层),避免<div><div><div><button>... <li>图标按钮必须包裹在 <code><button></button>内,并设min-width: 48px; min-height: 48px; - 用
fieldset分组复选框时,不要在外层再套一层<div class="wrapper"> —— 这会让 DOM 深度超限 真实场景里,最常被忽略的是「焦点迁移」和「事件冒泡深度」:前者让键盘用户卡在交互起点,后者让触摸用户觉得“点了没反应”。这两点不解决,其他 ARIA 属性加得再多,也只是表面合规。</div>











