只要一组数据必须同时看行和列才能读懂,它通常就适合放进表格。表格和普通块布局有个很直接的区别:同一列要共用列宽,同一行要共用行高,相邻单元格的边框还可能合成一条。一个单元格的内容变化,往往会影响整列甚至整张表格。
所以,浏览器不能把每个单元格分开算完就结束。它要先整理表格结构,建立行列网格,再处理背景、边框和尺寸。下面我们顺着这个过程来看,也会解释几个常见现象:为什么 td 上的 margin 没有拉开间距,为什么 col 能设置的样式很少,以及 table-layout: fixed 为什么主要读取第一行。
数据本身有行列关系时,优先使用 table、caption、thead、tbody、tfoot、th 和 td。把普通元素的 display 改成表格角色,只会改变排版方式,不会补上表头关联、键盘导航和辅助技术需要的语义。
浏览器拿到表格后,第一步不是计算列宽,而是确认表格、行组、行、列组、列、单元格和标题之间的关系。中间层缺失时,它还会补出匿名结构。等理论网格建好以后,浏览器才计算边框、宽高和对齐。
网格线围成的最小矩形叫网格单元。普通 td 一般占一个网格单元;设置了 rowspan 或 colspan 后,一个单元格会覆盖多个网格单元,但四条边仍要落在网格线上。这里说的网格单元只是浏览器内部的布局概念,不是 DOM 节点,也不能用选择器选中。
行、行组、列、列组和单元格都属于内部表格盒。它们有内容、内边距和边框,却不能靠普通外边距彼此分开。具体来说:
td、tr 或 tbody 设置 margin,不能创造单元格间距;border-spacing;caption 是例外,它更像放在表格前面或后面的块盒。建立网格时,浏览器按下面的关系安排各个盒:
同一列里的网格单元等宽,同一行里的网格单元等高。列与列之间可以宽窄不同,行与行之间也可以高低不同。理解这层联动,后面再看列宽和行高就容易多了。
HTML 表格元素自带对应的布局角色。CSS 也允许普通元素通过 display 生成同类表格盒,常用值如下:
table-header-group 在视觉上会排到其他行组前面,table-footer-group 则排到主体后面。分页打印时,浏览器还可能重复显示表头或表尾。不过,源码里的结构顺序仍要合理。视觉重排解决不了错误的表格语义。
CSS 表格以行为主。源码会明确写出一行行内容,浏览器再把各行的第一个、第二个、第三个单元格纵向对应起来,推导出列。一个单元格同时属于一行和一列,但在 DOM 里,它通常是行的后代,不是列的后代。
这也解释了为什么 col 和 colgroup 的样式能力有限。它们不是普通的可见盒,主要作用是把少量样式传给整列。实际有意义的非表格属性主要包括:
width:提供列的最小宽度约束,内容仍可能把列撑得更宽;background:只有上层的单元格与行背景透明时才能透出来;border:只在 border-collapse: collapse 时参与边框冲突;visibility: collapse:折叠该列并缩减表格占用宽度,跨列单元格相交部分会被裁切。其他 visibility 值在列上下文中不产生通常的效果。.records { display: table; }
.records__head { display: table-header-group; }
.records__body { display: table-row-group; }
.records__row { display: table-row; }
.records__cell { display: table-cell; padding: 0.75rem; }下面可以分别修改外层、中层和内层的表格角色。你可以故意把“行”或“单元格”改成普通块,看看浏览器为了组成合法表格,会怎样重新包装这些内容。
表格角色之间有固定的父子关系,但实际代码可能漏掉行、漏掉单元格,或者直接把普通文本放进表格。浏览器不会因此放弃排版,而是生成匿名表格对象,把结构补到可以布局的状态。这些对象只存在于格式化结构中,并不是 DOM 里的真实元素。
例如,table-cell 的父级不是 table-row 时,浏览器会把连续的单元格兄弟包进一个匿名行:
<div class="table">
<!-- 浏览器布局树中补出匿名 table-row -->
<span class="cell">产品</span>
<span class="cell">库存</span>
</div>常见的修补情况有七类:
table-cell 的父级不是 table-row:连续单元格外补匿名行。table-row 的父级不是表格、行内表格或行组:连续行外补匿名表格。table-column 的父级不是表格、行内表格或列组:连续列外补匿名表格。这里的“连续兄弟”很关键:只要遇到一个合法角色,当前匿名包装范围就会结束。匿名对象适合容错,不适合拿来设计正式结构。你不能给匿名行加类名、绑定事件或建立无障碍关联;调试时还会遇到 DOM 看着只有一层,布局结构却多出一层的情况。
不要把自动补结构当成日常写法。HTML 解析器也会修复非法 table 标记,而且这一步可能发生在 CSS 格式化之前。最后得到的 DOM、可访问性树和布局树,未必会按源码表面上的层级一一对应。
表格各层背景不是画在同一个平面上。你可以把它们理解成六张依次叠放的透明纸,从下到上分别是:
上层有不透明背景,就会挡住下面的颜色;上层透明,下面的背景才看得见。所以,给列设置背景后,如果行组、行或单元格又画了不透明背景,列的颜色仍然不会出现。单元格位于最上层,通常也是最终看到的颜色。

table { background: #fff4d6; }
col.is-emphasis { background: #dbeafe; }
tr:nth-child(even) { background: rgb(255 255 255 / 70%); }
td.is-alert { background: #fee2e2; }在这段样式里,透明单元格能透出行或列的背景;.is-alert 自己有红色背景,会盖住下面各层。如果偶数行改用不透明白色,那一行里的蓝色列背景也会被挡住。
caption 用来说明整张表格表达什么。它不是某一列的表头,也不是放在表格外面的普通页面标题。屏幕阅读器可以读取它来帮助用户判断表格用途,所以表格需要标题时,应把 caption 写在 table 内。
<table class="sales">
<caption>2026 年第二季度各区域销售额</caption>
<thead>...</thead>
<tbody>...</tbody>
</table>.sales caption {
caption-side: bottom;
padding: 0.75rem 0;
color: #475569;
text-align: left;
}caption-side 常用 top 和 bottom,初始值是 top,并且可以继承。视觉上,标题像一个放在表格盒前面或后面的块盒,可以设置背景、边框、内边距、文本对齐和外边距,也会从表格继承文字颜色等属性。它的宽度以表格内容宽度为参照。
把 caption-side 改成 bottom,改变的只是显示位置,标题仍然属于这张表格。HTML 里依旧建议把 caption 放在 table 的第一个子元素位置。
表格边框有两种处理方式,由 table 上的 border-collapse 决定:
separate:初始值。每个单元格保有自己的四条边框,单元格之间可以留出距离;collapse:相邻边框在共享网格线上竞争,最终只绘制一条获胜边框。
.spaced {
border-collapse: separate;
border-spacing: 0.5rem 0.75rem;
}
.ruled {
border-collapse: collapse;
}border-collapse 设置在 table 或 inline-table 角色上,并且可以继承。判断模型时不能只看两条线有没有挨在一起。分离模型即使把 border-spacing 设为 0,相邻单元格仍会各画一条边,只是两条边紧贴在一起;折叠模型会先裁决冲突,最后只画一条。
在分离模型里,每个单元格保留自己的边框。要拉开单元格之间的距离,应把 border-spacing 写在表格上,不能写在 td 上:
table {
border-collapse: separate;
border-spacing: 8px 12px; /* 水平、垂直 */
padding: 16px;
}只写一个长度时,水平和垂直间距相同。写两个长度时,第一个控制水平方向,第二个控制垂直方向。最外层单元格和表格内边距边缘之间也会保留这段间距。因此,从表格外框到最外层单元格的可见距离,大致是 table 的 padding 加上对应方向的 border spacing。
分离模型不会像处理单元格那样绘制行、行组、列和列组的边框,这些对象上的相关声明会被忽略。需要分区线时,可以直接给对应单元格加边框,或者用背景区分区域。
empty-cells 控制空单元格是否绘制背景和边框:
td { empty-cells: hide; }show:初始值,空单元格照常绘制背景和边框;hide:空单元格像 visibility: hidden 一样不绘制,但仍保留网格位置;这里的“空”不能只凭肉眼判断。图片、表单控件和非换行空格都算内容;普通空格、制表符和换行通常不算。整行单元格都为空,并且都使用 empty-cells: hide 时,这一行可以按不显示处理。
折叠模型没有单元格间隙。共享边框以一条假想网格线为中心,一半宽度落在线的这一侧,另一半落在另一侧。表格最外侧的边框同样可能有一半伸到表格计算宽度之外。表格需要和外部元素精确对齐时,这半条边框也要算进去。
切到折叠模型后,不能再靠 table 自身的 padding 拉开外框和最外层单元格。单元格、行、行组、列、列组和表格本身的边框,都可能在同一个位置相遇。
浏览器按下面的顺序选出最终边框:
hidden:任何候选边框为 hidden,该位置的所有边框都不绘制;它不是普通的“无边框”,而是最高优先级的抑制命令。double、solid、dashed、dotted、ridge、outset、groove、inset、none。
border-style: hidden 和 none 的结果不同。none 的优先级最低,通常会输给相邻的可见边框;hidden 的优先级最高,会让共享位置完全不画线。发现相邻元素的边框也一起消失时,可以先检查这里有没有使用 hidden。
你可以切换边框模型,再调整间距和空单元格策略。重点看 border-spacing 与 empty-cells 在哪种模型中生效,以及折叠模型怎样处理那条更宽的双线边框。
table-layout 决定浏览器用哪种算法计算列宽:
table { table-layout: auto; } /* 初始值 */
table { table-layout: fixed; }两种算法都允许设置宽度。真正的区别是,哪些信息会参与列宽计算。自动布局会查看所有单元格的内容和宽度约束;固定布局主要读取表格宽度、列定义和第一行,后面的行不能反过来修改已经确定的列宽。

行数很多、列宽需要保持稳定时,可以考虑固定布局,例如日志列表或管理后台的数据表。各列内容长度差异很大,希望浏览器按内容分配空间时,自动布局更合适。无论使用哪一种,窄屏下通常都要在外层容器设置 overflow-x: auto,避免把内容硬挤到无法阅读。
固定布局按下面的顺序分配列宽:
col 或 colgroup 上非 auto 的宽度先确定对应列;auto 宽度;若第一行单元格跨多列,该宽度会分配到所跨列;第一行之后再给单元格声明宽度,不会改变已经确定的列宽。内容放不下时,需要由单元格的 overflow、换行或文本截断规则来处理:
.data-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.data-table td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}<table class="data-table">
<colgroup>
<col style="width: 8rem" />
<col style="width: 35%" />
<col />
</colgroup>
...
</table>固定布局最好配合明确的表格宽度使用。如果表格宽度仍是 auto,浏览器可能改用自动布局的规则。实际项目里,不要只写 table-layout: fixed,却完全不说明表格能占多宽。
自动布局要查看整张表格。哪怕长单词、图片或表单控件只出现在最后一行,也可能抬高整列的最小宽度。理解这套算法时,可以先把每个单元格看成一个可伸缩区间,再把这些区间汇总到列,最后计算整张表格需要多宽。
浏览器先估算每个单元格的两个宽度:
width 若更大,会抬高这个下限;接着把单元格的约束汇总到列:
列的范围确定后,再计算表格本身的宽度:
auto 宽度时,将该宽度与“所有列宽 + 边框 + 单元格间距”比较,最终不能小于容纳列所需的总宽度;多余空间再分给列;auto 时,通常由列内容、边框和间距共同形成所需宽度;百分比宽度作为约束参与,但并不意味着所有场景都能被精确满足。min-width、max-width、不可断行字符串、图片固有尺寸、white-space 和断词策略也会影响结果。实际排查时,不必先追每一个中间数值。先记住一件事:自动布局会参考全表内容,width 是参与协商的约束,不一定是最终得到的绝对宽度。
表格高度很难像列宽那样精确控制。height: auto 时,总高度来自各行高度、边框,以及分离模型中的间距。浏览器会根据单元格内容估算每一行需要多高,再把所有行排在一起。
显式设置的表格高度更接近一个下限。指定值小于内容实际需要的高度时,内容通常还是会把表格撑高。指定值大于所有行的总高度时,常见浏览器会把多余空间分给各行,但不适合依赖这个分配过程做像素级布局。
下面几种情况在不同浏览器之间更难精确推算:
实际开发中,让内容决定行高通常更稳。需要让各行看起来更整齐时,可以先调整单元格 padding 和 line-height,再给单元格内部的包装元素设置类似 min-height 的约束。直接把整张表格锁成固定高度,往往更容易被真实内容撑破。
水平方向用 text-align,它控制单元格内行内内容的位置:
th { text-align: left; }
td.number { text-align: right; font-variant-numeric: tabular-nums; }垂直方向用 vertical-align。在单元格里,最常用的是下面四个值:
top:内容顶部与当前行顶部对齐;跨行时与所跨第一行顶部对齐;middle:内容在当前行或所跨多行的整体高度中居中;bottom:内容底部与当前行底部对齐;跨行时与所跨最后一行底部对齐;baseline:单元格首行文字基线与行基线对齐。一行里如果有多个单元格使用基线对齐,浏览器会找到位置最低的首行基线,把它定为这一行的基线,其他单元格再补足上方空间。没有单元格使用基线对齐时,这一行不必建立可用基线。最终效果可以理解为:浏览器在单元格顶部或底部补空间,把内容放到 top、middle、bottom 或 baseline 对应的位置。
sub、super、text-top 和 text-bottom 原本是为行内盒设计的,用在表格单元格上不适合作为稳定的垂直排版方案。要控制单元格内容位置,明确使用上面的四个常用值即可。
这个实验把宽度算法、长内容和垂直对齐放在同一张表里。你可以先在 auto 和 fixed 之间切换,再加入长内容。观察自动布局是否重新分配列宽,以及固定布局怎样把超出的内容交给溢出规则处理。
用 margin 制造单元格间距。 内部表格盒不会像普通盒那样用 margin 彼此分隔。需要间距时,要根据边框模型选择 border-spacing 或单元格内边距。
把 col { width: ... } 当成绝对宽度。 在自动布局中,它提供的是约束和下限,内容仍可能把列撑宽。需要稳定分配列宽时,固定布局更合适。
认为 border-spacing: 0 等于折叠边框。 分离模型仍保留相邻单元格各自的边框,只是两条边贴在一起。折叠模型才会在冲突后只画一条边。
把 hidden 当作 none。 在折叠边框冲突中,hidden 会压过所有候选并取消绘制,none 的优先级却是最低的。
使用固定布局却不处理溢出。 列宽稳定以后,长内容不能再反向撑宽这一列,所以还要明确选择换行、断词、省略或滚动策略。
为了外观用 div 模拟数据表。 表格 display 角色只能得到相应的视觉排版,不会自动生成 caption、表头关联和数据表语义。
准备上线时,可以顺着下面的问题检查一遍:
caption、th 和合适的 scope;colgroup 和溢出策略;表格出现异常时,可以先把问题放进三条线索里:
别急着增加选择器。先问三个具体问题:浏览器补出了什么结构?当前颜色或边框来自哪一层?正在使用的列宽算法会读取哪些内容?把这三点对上,通常就能确定问题发生在格式化、绘制还是尺寸计算阶段。