跳到主要内容
全部文章

2026年9月15日·阅读约 2 分钟

表格标题无障碍:大多数网站遗漏的 WCAG 规则

没有真正标题标记的数据表格,在屏幕阅读器里读起来就是毫无意义的数字。了解 WCAG 1.3.1 的修复方法,以及 Axeazy 能与不能自动修复什么。

wcagaccessibilitytablestable-headerswcag-1-3-1axe-corescreen-readerhtml

没有真正标题标记的数据表格,在屏幕阅读器里读起来就是一串光秃秃的数字,完全不知道每个数字属于哪一行或哪一列。 修复的办法是:标题单元格用 <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> 的区别就是一切

两个看起来一模一样的表格标题,一个用 td 构建,一个用 th 构建,只有一个会被屏幕阅读器识别为标题。

视力正常的访客阅读表格时,会不假思索地扫过一行,再向上看列标题。屏幕阅读器没有等效的“抬眼一瞥”的办法。它逐个单元格朗读,而它知道某个单元格是标题而非普通数据的唯一方式,就是底层的 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 属性,以及什么时候真正需要它

示意图:在数据表格中,scope=col 指向一整列,scope=row 指向一整行。

对于简单表格,也就是标题只在第一行,或只在第一列,而不是两者都有的表格,只用 <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 属于哪个单元格,都需要理解表格的实际含义,这仍然是人的工作。

如何检查您自己的表格

您不需要安装屏幕阅读器就能发现这个错误最常见的形式。

  1. 查看页面源代码。 右键点击网站上的任意表格,选择“查看页面源代码”,或打开 DevTools 查看 Elements 面板。搜索 <th。如果看起来像标题的那一行是由 <td> 元素构成的,这个表格就不通过。
  2. 检查多方向表格上的 scope 值。 如果表格同时有横向在上方和纵向在侧边的标题,请确认每个 <th> 都带有 scope="col" 或 scope="row",而不只是加粗样式。
  3. 听一听。 在 NVDA 中,用表格导航(Ctrl+Alt+方向键)在标记正确的表格中移动时,会在每个单元格的值旁读出相关标题。如果您只听到没有先读标题的原始数字,说明表格没有被正确关联。
  4. 或者运行扫描,而不是逐个表格检查。 Axeazy 的免费扫描大约 60 秒就能按上述全部七条规则检查您首页上的每个表格,同时涵盖它支持的其他修复类别。

常见错误

  • 用加粗的 <td> 单元格代替 <th>。 最常见的失败,也最难靠肉眼发现,因为它看起来完全正确。
  • 表格标题说明只是表格上方一行加粗的文字,而不是真正的 <caption> 元素。视力正常的访客看不出区别,屏幕阅读器却能。
  • 同一页面上的多个表格没有区分性的标题说明,导致屏幕阅读器用户在表格之间跳转时分不清哪个是哪个。
  • 仅为视觉排版而使用表格,而不是用于表格数据。屏幕阅读器仍会尝试读出本不该存在的行列关系,这比完全没有结构更令人困惑。现代 CSS(Grid 或 Flexbox)可以处理排版,没有这种副作用。
  • 在表格实际需要 headers/id 时却使用 scope。 scope 只能描述简单的行或列关系。有合并单元格或标题不整齐位于某一行或某一列的表格,需要更明确的 headers/id 配对。

Axeazy 在这里能修复什么

流程图:只有拼错的 scope 值会被自动修复,其他所有表格无障碍问题都被标记为需手动审核。

这一类别比大多数更适合坦率地谈,因为其中自动化的部分确实很小。

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" 可以让辅助技术完全跳过表格语义。

参考来源

想在邮箱中收到这类内容吗?

The ADA Alert。每周资讯。

每周二:无障碍新闻、常见 WCAG 问题以及代码层面的修复。免费。一键退订。 本简报以英文撰写。

我们交付的每份报告的范围: 范围限制:本报告记录的是使用 axe-core v4.11.0 进行 WCAG 2.2 AA 自动扫描所发现的违规问题。自动化工具无法测试所有 WCAG 准则;许多准则需要人工审查。本报告不构成法律建议,不保证符合 ADA,也不能防止法律诉讼。是否符合 ADA 是由法院作出的法律认定。如需完整的 WCAG 符合性评估,请聘请获得 IAAP 认证的无障碍专业人士。