非原生表单组件必须同时设置role、tabindex="0"和aria-required="true"才能被读屏识别;仅aria-required无效,且必须作用于最终聚焦的输入节点,而非容器或label。

非原生表单组件必须用 role + aria-required 才能被读屏识别
原生 input、textarea、select 有内置语义,aria-required="true" 只是补充;但用 div 或 span 模拟输入框时,浏览器完全不知道这是个“可填字段”,光加 aria-required="true" 没用——它会被静默忽略。
必须同时满足两个条件:一是通过 role 明确控件类型(如 role="textbox"、role="combobox"),二是加上 tabindex="0" 让其可聚焦,最后才是 aria-required="true"。
-
role决定读屏怎么称呼它:“编辑框”还是“下拉列表”,不能乱写(比如role="button"配aria-required就无效) -
tabindex="0"是硬性要求:不可聚焦的元素,辅助技术不会把它当表单控件处理 - 别在包裹容器上写
aria-required,必须落在最终接收焦点和输入的那个 DOM 节点上
示例(合法):
<div role="textbox" aria-required="true" tabindex="0" contenteditable="true"></div>
React/Vue 自定义组件里 aria-required 容易漏传
框架封装的 MyInput 组件,required 属性常被透传到内部 input,但 aria-required 几乎总是被丢弃——尤其当组件用了 div 包裹、或改用 role="textbox" 时。
立即学习“前端免费学习笔记(深入)”;
检查方法很直接:打开 DevTools → Elements → 找到渲染出来的实际输入节点 → 确认 required 和 aria-required="true" 都在它上面。不在?那就得手动透传。
- React 中,别只 spread
...props,要显式取required并同步设aria-required - Vue 中,
v-bind="$attrs"不会自动带aria-required,需在模板里单独写:aria-required="required ? 'true' : 'false'" - 如果组件内部用的是
role="textbox",那required属性本身不生效,此时aria-required="true"就是唯一传达必填语义的手段
动态切换必填状态时,DOM 属性必须同步更新
JS 控制 input.required = true 时,很多人只改了原生属性,却忘了 aria-required 和视觉标识(比如星号)。结果屏幕阅读器仍播报“可选”,用户也看不到提示。
关键点:必填状态是三者联动——DOM 属性、视觉反馈、ARIA 状态缺一不可。
- 不要只操作 JS 变量或 state,必须真实写入 DOM:
el.setAttribute('aria-required', 'true') - 配合
aria-invalid="true"处理校验失败:仅标aria-required不代表校验逻辑已就位 - 避免用
aria-required="false"和required共存,部分读屏会跳过整个字段甚至报错
aria-required="true" 单独存在等于没标
它不阻止提交、不触发浏览器验证、不影响移动端键盘类型、也不让 :required CSS 伪类生效。只对屏幕阅读器“说话”,而且前提是该元素已被正确识别为输入控件。
常见错误现象:aria-required="true" 加在 label、span 或未设 role 的 div 上,DevTools 里看着有,但 NVDA/VoiceOver 根本不读“必填”二字。
- 对
type="checkbox"或type="radio"组合,单独标某个项的aria-required无意义——应校验“至少一个被选中”,靠 JS +aria-invalid实现 - 按钮、链接、纯展示文本等非输入元素,加
aria-required属于误用,会被辅助技术忽略 - 真正起作用的永远是那个最终接收焦点、接受输入的节点,不是它的父容器或 label



















