legend必须是fieldset的第一个子元素且非空,否则nvda或voiceover会跳过整组,document.activeelement无法捕获焦点;fieldset[disabled]仅禁用直接子级表单控件,嵌套过深或框架剥离语义均会导致可访问性失效。

legend 必须是 fieldset 的第一个子元素且非空
浏览器和读屏软件只认 fieldset 开头紧跟着 legend 这一种结构。中间插个 <p></p>、注释、空格,甚至一个换行符,都可能让语义链断裂——NVDA 或 VoiceOver 直接跳过整组,document.activeElement 也抓不到焦点路径。
-
<fieldset> <legend>收货地址</legend> <input name="city"> </fieldset>✅ 正确 -
<fieldset> <input name="city"><legend>收货地址</legend> </fieldset>❌legend不是首子元素,可访问性失效 -
<fieldset> <legend></legend> <input name="city"> </fieldset>❌ 空legend被播报为“空白分组”,比不写更误导 -
<fieldset><legend><h3>收货地址</h3></legend></fieldset>❌h3干扰名称计算,部分读屏器静音或跳过
动态渲染(Vue/React/Jinja)时最容易漏掉这点:条件分支只包了 input,没把 legend 一起包进去,结果渲染出孤立的 legend 节点,WCAG 检查直接报 fieldset-missing-legend。
fieldset[disabled] 只禁用直接子级表单控件
fieldset 加 disabled 不是加灰样式,而是触发原生表单行为:所有**直接子级**标准控件自动失效、无法聚焦、提交时值被排除。
- 生效范围:
<input>、<select></select>、<textarea></textarea>、<button></button>、<input type="radio">组 - 不生效范围:包在
<div> 里的 <code>input、contenteditable元素、自定义 Web Component(除非主动监听状态) - 被禁用的控件值不会出现在
FormData中;若业务要求提交,需 JS 手动补全,例如formData.append('email', input.value) -
<label></label>点击行为在 Chrome 和 Firefox 中不一致:Chrome 允许穿透点击禁用控件,Firefox 直接拦截,务必真机测试 - 优先用单层
fieldset分组,靠 CSS(如margin、border-bottom)做视觉区隔 - 若必须嵌套,确保每层都有语义清晰的
<legend></legend>,且legend内不放任何交互元素(如<button></button>) - 用
document.activeElement+console.log实时观察焦点路径,比依赖默认 Tab 顺序更可靠 - 打开开发者工具,检查渲染后 DOM,确认
fieldset和legend标签真实存在且未被 JS 清除 - 若框架不支持,可绕过其表单组件,手写原生
fieldset+legend,再用 CSS 对齐样式 -
form属性允许fieldset脱离<form></form>物理包裹但仍参与提交,但仅限 HTML5,且需确保目标form的id存在
嵌套 fieldset 容易破坏键盘 Tab 导航流
嵌套本身合法,但 ≥3 层深层嵌套极易让键盘用户卡住——按 Tab 进入内层后,很难再跳出,尤其当某层 legend 缺失或为空时,焦点管理逻辑会失效。
UI 框架常剥离 fieldset 语义,必须检查最终 DOM
Ant Design、Bootstrap 等框架的表单组件常通过 CSS Grid/Flex 模拟分组结构,会主动移除或忽略你写的 fieldset 和 legend——此时它们只剩视觉作用,语义和可访问性全丢。
真正难的不是手写一次正确结构,而是每次动态生成都守住 legend 必须是 fieldset 第一个子元素且仅含纯文本这条线——它卡在语义链最前端,错一点,整组控件的可访问性就断了。











