thead 的作用是定义列的语义范围,必须按 thead→tfoot→tbody 顺序书写,仅包含 tr 和 th,且列数须与 tbody 一致,否则导致可访问性、打印、sticky 表头等功能失效。

thead 的作用不是“加个样式容器”,而是告诉浏览器和辅助技术:“这些行定义了列的语义范围”。它必须按规范写,否则表头在滚动、打印、读屏时会失效。
thead 必须写在 tbody 前面,且顺序不能调换
HTML 规范强制要求 thead → tfoot → tbody 的 DOM 顺序。这不是为了好看,而是解析逻辑依赖:
- 浏览器流式渲染时,先看到
thead就立刻画出表头,用户不用等几百行数据加载完 -
tfoot写在tbody前,能让打印引擎提前知道“汇总行长什么样”,实现每页重复显示 - 如果写成
<table> <tbody>...</tbody> <thead>...</thead> </table>,部分旧版 Safari 会忽略position: sticky行为,JS 读取table.tHead返回的节点位置也可能错乱
thead 里只能放 tr,单元格必须用 th
thead 不是“第一行数据区”,而是列定义区。它的内容要能回答“这一列代表什么”:
- 写
<td>姓名</td>在thead里,屏幕阅读器只会读作“姓名”,不会把它和下面所有td关联 - 写
<th scope="col">姓名</th>,辅助技术就知道“这一列所有单元格都属于‘姓名’”,自动建立语义映射 -
scope="col"是显式声明作用域,虽然现代浏览器对省略它有一定容错,但 WCAG 合规和 PDF 导出(如 jsPDF autoTable)常依赖它 - 空
thead(比如<thead></thead>)会导致某些旧版 Safari 渲染异常,打印时表头消失
多级表头要用嵌套 tr + rowspan/colspan
复杂表头(比如“销售额”下分“Q1/Q2/Q3”)不是靠多个 thead,而是在一个 thead 内用多行 tr 实现层级:
- 第一行
tr用colspan合并大类,如<th colspan="3">销售额</th> - 第二行
tr拆解具体列,如<th>Q1</th> <th>Q2</th> <th>Q3</th> - 所有
tr的总列数必须与tbody中每行的td数一致,否则表格错位或验证失败 - 跨行标题(如“产品名称”垂直居中)要用
rowspan,但顶层th仍必须在thead内,不能挪到tbody里
动态渲染时最容易漏掉结构约束
Vue/React 生成表格时,常把 thead 和 tbody 分开写在不同组件里,结果 DOM 顺序被破坏:
立即学习“前端免费学习笔记(深入)”;
- 组件 A 输出
<tbody>...</tbody>,组件 B 输出<thead>...</thead>,拼在一起就变成<tbody></tbody> <thead></thead> - 即使 CSS 用
order调整视觉顺序,DOM 结构错乱仍会导致打印分页失效、sticky 表头不粘、table.tFoot返回 null - 解决方案:把整个
table包裹在一个组件里,用v-if或条件渲染控制thead/tfoot/tbody的内容,但不拆分标签层级
thead”,而是它是否出现在正确位置、是否只含语义正确的元素、是否与 tbody 列数对齐——这三点漏掉任何一点,固定表头、可访问性、分页打印都会悄悄失效。



















