2026年9月15日·阅读约 2 分钟
表格标题无障碍:大多数网站遗漏的 WCAG 规则
没有真正标题标记的数据表格,在屏幕阅读器里读起来就是毫无意义的数字。了解 WCAG 1.3.1 的修复方法,以及 Axeazy 能与不能自动修复什么。
没有真正标题标记的数据表格,在屏幕阅读器里读起来就是一串光秃秃的数字,完全不知道每个数字属于哪一行或哪一列。 修复的办法是:标题单元格用 <th> 代替 <td>,并添加 scope 属性。这满足 WCAG 1.3.1(信息和关系),让表格从单纯的视觉排版变成屏幕阅读器真正可以浏览的内容。

要点
- 如果样式相同,
<th>和<td>看起来一模一样,但只有<th>会告诉屏幕阅读器“这是标题”。<td>里加粗居中的文字,无论看起来如何,对辅助技术来说都不是标题。 scope属性告诉屏幕阅读器标题适用于哪个方向。 有效值只有col、row、colgroup和rowgroup,没有其他。- 这是 WCAG 1.3.1(信息和关系)的要求,A 级。 它还对应 Section 508 的 1194.22(g) 和 EN 301 549 的 9.1.3.1,因此不只是美国的规则。
- axe-core 会扫描七种不同的表格问题,其中只有一种(拼错的
scope值)目前有自动修复。其余的都需要一个懂得表格实际含义的人。 - 简单表格(标题只在第一行或第一列)所需的标记比大多数指南暗示的要少。 那里只用普通的
<th>元素就够了;当表格在不止一个方向上有标题时,scope才变得必要。
为什么 <th> 与 <td> 的区别就是一切

视力正常的访客阅读表格时,会不假思索地扫过一行,再向上看列标题。屏幕阅读器没有等效的“抬眼一瞥”的办法。它逐个单元格朗读,而它知道某个单元格是标题而非普通数据的唯一方式,就是底层的 HTML 元素。
<th> 是标题单元格。浏览器默认将其加粗并居中,屏幕阅读器在用户浏览表格时会将其读作标题。<td> 是普通数据单元格。无论对它应用什么 CSS,它都不会带有这样的提示。
几乎所有现实中的表格都坏在这里。开发人员做了一个表格,想让标题行看起来加粗,于是用 <td style="font-weight: bold"> 而不是 <th>。视觉上,它与真正的标题无法区分。对屏幕阅读器来说,它只是另一个碰巧加粗的数据单元格,与下方的单元格毫无关系。
<!-- 失败:看起来像标题行,却被读作普通数据 -->
<table>
<tr>
<td><strong>Day</strong></td>
<td><strong>Open</strong></td>
<td><strong>Close</strong></td>
</tr>
<tr>
<td>Monday</td>
<td>9:00 AM</td>
<td>6:00 PM</td>
</tr>
</table>
<!-- 通过:真正的标题单元格,被读作标题,外加表格标题说明 -->
<table>
<caption>Store hours</caption>
<tr>
<th scope="col">Day</th>
<th scope="col">Open</th>
<th scope="col">Close</th>
</tr>
<tr>
<td>Monday</td>
<td>9:00 AM</td>
<td>6:00 PM</td>
</tr>
</table>
两者对视力正常的访客呈现完全一样。只有第二个能告诉屏幕阅读器用户,“9:00 AM”属于 “Monday” 的 “Open”。
scope 属性,以及什么时候真正需要它

对于简单表格,也就是标题只在第一行,或只在第一列,而不是两者都有的表格,只用 <th> 就够了。屏幕阅读器可以正确推断方向。在这种情况下,WCAG 的严格要求其实比许多无障碍内容暗示的要轻。
一旦表格同时在两个方向上有标题,例如日程表上方是星期、侧边是时间段,scope 在实践中就不再是可选的了。没有它,屏幕阅读器没有可靠的办法知道某个 <th> 标注的是它所在的行还是列。
<table>
<caption>Weekly class schedule</caption>
<tr>
<th scope="col"></th>
<th scope="col">Monday</th>
<th scope="col">Wednesday</th>
</tr>
<tr>
<th scope="row">9:00 AM</th>
<td>Yoga</td>
<td>Pilates</td>
</tr>
<tr>
<th scope="row">6:00 PM</th>
<td>Spin</td>
<td>Yoga</td>
</tr>
</table>
这里,scope="col" 标记星期标题,scope="row" 标记时间标题。屏幕阅读器用户移到 “Pilates” 单元格时会同时听到两者:Wednesday、9:00 AM、Pilates。
对于真正不规则的表格,也就是有合并单元格、标题不整齐地位于某一行或某一列的表格,仅靠 scope 是不够的。这就是 headers 和 id 属性的用途:每个数据单元格明确列出适用于它的每个标题单元格的 id。
<th id="q3">Q3</th>
...
<td headers="q3">42</td>
这种写法编写和维护都更费功夫,这正是它只保留给 scope 确实无法描述的表格、而不作为默认做法的原因。
axe-core 究竟检查表格的哪些方面
axe-core 就是 Chrome DevTools 无障碍面板和 Axeazy 扫描器背后的同一个引擎,它对表格标记运行七项独立检查。目前只有一项有自动修复。
| 规则 | 检查内容 | 修复路径 |
|---|---|---|
scope-attr-valid |
scope 的值不是真正的单词,是 col/row/colgroup/rowgroup 的拼写错误或复数 |
通过 GitHub pull request 自动修复,范围很窄:只处理可识别的拼写错误 |
empty-table-header |
存在 <th> 但里面没有文字 |
手动 |
th-has-data-cells |
存在 <th> 但没有任何数据单元格使用它 |
手动 |
td-has-header |
某个数据单元格完全没有关联的标题 | 手动 |
td-headers-attr |
headers 属性指向的 id 不是真正的标题单元格 |
手动 |
table-duplicate-name |
同一页面上的两个表格具有相同的可访问名称 | 手动 |
table-fake-caption |
文字被设置成看起来像标题说明,而不是真正的 <caption> 元素 |
手动 |
这张表让真实情况一目了然。表格结构绝大部分属于手动修复的类别。唯一自动写入的部分,是更正已经存在但拼错的 scope 值,比如把 "cols" 或 "ROW " 规范化为 "col" 或 "row"。添加缺失的标题、编写表格标题说明,或确定哪个 headers 的 id 属于哪个单元格,都需要理解表格的实际含义,这仍然是人的工作。
如何检查您自己的表格
您不需要安装屏幕阅读器就能发现这个错误最常见的形式。
- 查看页面源代码。 右键点击网站上的任意表格,选择“查看页面源代码”,或打开 DevTools 查看 Elements 面板。搜索
<th。如果看起来像标题的那一行是由<td>元素构成的,这个表格就不通过。 - 检查多方向表格上的
scope值。 如果表格同时有横向在上方和纵向在侧边的标题,请确认每个<th>都带有scope="col"或scope="row",而不只是加粗样式。 - 听一听。 在 NVDA 中,用表格导航(
Ctrl+Alt+方向键)在标记正确的表格中移动时,会在每个单元格的值旁读出相关标题。如果您只听到没有先读标题的原始数字,说明表格没有被正确关联。 - 或者运行扫描,而不是逐个表格检查。 Axeazy 的免费扫描大约 60 秒就能按上述全部七条规则检查您首页上的每个表格,同时涵盖它支持的其他修复类别。
常见错误
- 用加粗的
<td>单元格代替<th>。 最常见的失败,也最难靠肉眼发现,因为它看起来完全正确。 - 表格标题说明只是表格上方一行加粗的文字,而不是真正的
<caption>元素。视力正常的访客看不出区别,屏幕阅读器却能。 - 同一页面上的多个表格没有区分性的标题说明,导致屏幕阅读器用户在表格之间跳转时分不清哪个是哪个。
- 仅为视觉排版而使用表格,而不是用于表格数据。屏幕阅读器仍会尝试读出本不该存在的行列关系,这比完全没有结构更令人困惑。现代 CSS(Grid 或 Flexbox)可以处理排版,没有这种副作用。
- 在表格实际需要
headers/id时却使用scope。scope只能描述简单的行或列关系。有合并单元格或标题不整齐位于某一行或某一列的表格,需要更明确的headers/id配对。
Axeazy 在这里能修复什么

这一类别比大多数更适合坦率地谈,因为其中自动化的部分确实很小。
Axeazy 的扫描一旦发现上述七条表格规则的问题就会标记出来,无论是 Axeazy、Chrome DevTools,还是任何基于 axe-core 的工具,检测结果都是一样的。写出修复内容才是需要手动的地方。目前唯一能进入真实 GitHub pull request 的表格相关修复,是更正已存在的、属于可识别拼写错误或复数的 scope 值,该类别中的其他任何问题都还没有自动化路径。
目前 WordPress 插件也完全没有处理表格修复的途径。免费插件的三项即时修复涵盖跳过导航、语言属性和替代文字后备,都不涉及表格标记,付费方案目前也没有扩展到表格结构。
这不是有人遗忘的缺口。判断某个 <td> 是否本应是 <th>,或者一个不规则单元格究竟属于哪个标题,需要理解表格想要表达什么。脚本在这里猜错,会悄悄地向恰恰依赖标记来理解数据的那些人歪曲数据,这比标记出来交给人正确修复更糟。查看完整的修复类别说明。
常见问题
每个标题单元格都需要 scope 属性吗?
不需要。对于标题只在第一行或只在第一列的简单表格,普通的 <th> 元素本身就足够了。当表格同时在两个方向上有标题时,scope 才变得必要,比如上方是星期、侧边是时间的日程表。
scope 与 headers/id 有什么区别?
scope 描述简单的单方向关系:一个标题适用于整行、整列,或一组行或列。headers/id 更明确:每个数据单元格通过 id 精确列出哪些标题单元格适用于它。规则的网格状表格用 scope,只有带合并单元格或标题不整齐位于某行某列的不规则表格才用 headers/id。
Axeazy 会自动修复缺失的表格标题吗?
大多数不会,值得坦率地说明。目前表格相关修复中唯一有可用自动路径的,是更正已存在但拼错的 scope 值。添加缺失的标题、编写真正的表格标题说明,或把 headers 映射到正确的 id,都需要理解表格的含义,因此它们仍属于手动审核项。
每个表格都需要表格标题说明(caption)吗?
它本身并非 WCAG 1.3.1 的严格要求,但确实有用,尤其是当页面上有不止一个表格时。表格标题说明为表格提供可访问名称,让在表格之间跳转的屏幕阅读器用户知道自己到了哪一个。
用于页面布局而不是数据的表格呢?
尽可能避免。屏幕阅读器仍会尝试读出布局表格上的行列关系,造成用纯 CSS 布局时不会有的困惑。如果遗留的布局表格无法立即移除,给它加上 role="presentation" 可以让辅助技术完全跳过表格语义。
参考来源
我们交付的每份报告的范围: 范围限制:本报告记录的是使用 axe-core v4.11.0 进行 WCAG 2.2 AA 自动扫描所发现的违规问题。自动化工具无法测试所有 WCAG 准则;许多准则需要人工审查。本报告不构成法律建议,不保证符合 ADA,也不能防止法律诉讼。是否符合 ADA 是由法院作出的法律认定。如需完整的 WCAG 符合性评估,请聘请获得 IAAP 认证的无障碍专业人士。