HTML 表格几乎和网络本身一样古老,并且几十年来一直被使用——甚至滥用。曾几何时,由于缺乏其他合适的方法,勇敢的网页设计师使用表格拼凑出复杂的布局。幸运的是,这种做法如今已不再常见,但表格仍然是网络的重要组成部分,对于显示二维数据(按行和列组织的表格数据)来说至关重要。
一张设计精美的 HTML 表格,显示英国、阿富汗、澳大利亚、肯尼亚、洪都拉斯和加拿大的平均温度变化,以及全球平均温度
在网络上创建美观的表格有时会很棘手。我们将探讨一些关于使用 HTML 和 CSS 构建简单及复杂表格的技巧和注意事项,以及现代 CSS 如何帮助我们简化这一过程。
表格的元素
我们想要展示的数据是 2022 年不同国家的平均温度变化(相较基线)。数据来源于 国际货币基金组织。
像任何一篇高质量的文章一样,我们会从扎实的标记语言开始构建。这意味着,即使没有任何额外样式,我们的表格仍然能够清晰呈现,同时可以被屏幕阅读器正确解析。
<table>
元素 是包裹表格内容的核心元素。
<thead>
和<tbody>
分别包含表格的表头和表体行。<tr>
组成表格的行,每行包含<th>
(表头单元格) 和<td>
(表体单元格)。- 此外,我们还使用了
<tfoot>
(表尾) 元素,用于在表格底部显示全球平均温度变化。
以下是基础的 HTML 表格代码:
<table>
<thead>
<tr>
<th>Country</th>
<th>Mean temperature change (°C)</th>
</tr>
</thead>
<tbody>
<tr>
<th>United Kingdom</th>
<td>1.912</td>
</tr>
<tr>
<th>Afghanistan</th>
<td>2.154</td>
</tr>
<tr>
<th>Australia</th>
<td>0.681</td>
</tr>
<tr>
<th>Kenya</th>
<td>1.162</td>
</tr>
<tr>
<th>Honduras</th>
<td>0.945</td>
</tr>
<tr>
<th>Canada</th>
<td>1.284</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>Global average</th>
<td>1.4</td>
</tr>
</tfoot>
</table>
我们的行和列都包含表头单元格(<th>
元素)。为了确保内容能被辅助技术(如屏幕阅读器)更好地解析,我们可以为 <th>
元素添加 scope
属性,用以指定表头的作用域(行或列)。
虽然对于这种简单的表格来说这不是绝对必要的,但当表格扩展为更复杂的形式时,这种做法将非常有用。
以下是添加了 scope
属性 的表格代码:
<table>
<thead>
<tr>
<th scope="column">Country</th>
<th scope="column">Mean temperature change (°C)</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">United Kingdom</th>
<td>1.912</td>
</tr>
<tr>
<th scope="row">Afghanistan</th>
<td>2.154</td>
</tr>
<tr>
<th scope="row">Australia</th>
<td>0.681</td>
</tr>
<tr>
<th scope="row">Kenya</th>
<td>1.162</td>
</tr>
<tr>
<th scope="row">Honduras</th>
<td>0.945</td>
</tr>
<tr>
<th scope="row">Canada</th>
<td>1.284</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Global average</th>
<td>1.4</td>
</tr>
</tfoot>
</table>
最后,我们为表格添加一个标题,用来概括表格的内容。我们将使用 <caption>
元素 来实现,它必须是 <table>
元素 的第一个子元素。
以下是完整的代码示例:
<table>
<caption>2022 年的年度地表温度变化</caption>
<thead>
<tr>
<th scope="column">国家</th>
<th scope="column">平均温度变化 (°C)</th>
</tr>
</thead>
<tbody>
...
</tbody>
<tfoot>
...
</tfoot>
</table>
W3C 的 Web 可访问性倡议(WAI) 提供了多种示例,展示如何以可访问的方式为表格添加更复杂的摘要内容。
用户代理样式
如果我们在浏览器中查看表格,它目前看起来不太美观。表头和标题都是居中对齐的,无法区分表头、页脚和正文行。
一个未添加样式的 HTML 表格,显示英国、阿富汗、澳大利亚、肯尼亚、洪都拉斯和加拿大的平均温度变化,以及全球平均值
如今,各浏览器在表格的默认样式方面表现得相对一致。如果我们在浏览器的开发者工具中检查 <table>
元素,可以看到以下样式被应用:
table {
display: table;
border-spacing: 2px;
border-collapse: separate;
box-sizing: border-box;
text-indent: 0;
}
其中一些属性对我们有用。例如,浏览器会对 <th>
元素 应用 font-weight: bold
,从而在未应用任何其他样式之前就能区分表头与正文单元格。即使完全没有自定义 CSS,也可以清楚地看到行和列的标题与其对应数据之间的关系,提供了一个最低限度可用的用户体验。
然而,有些用户代理样式可能不太适用。我们可以应用一些基本的 CSS 来“重置”表格样式,使其更易于设计。
注意
无论做什么,都不要为了方便样式设置而将表格的 display
属性 修改为 grid
。这样会破坏浏览器为表格提供的内置可访问性功能,使得表格对使用辅助技术的用户变得不可用。
使用现代 CSS 重置表格样式
假设我们在 CSS 文件中对 <body>
设置了字体样式。表格会继承这些字体样式,从而使其外观有所改进。
body {
font-family: 'Open Sans', sans-serif;
line-height: 1.5;
}
将文本左对齐是快速提升表格可读性的另一种方法(说真的,为什么要将行标题居中对齐呢?!)。在整个表格上设置 text-align
属性 可以轻松实现,同时允许以后为单独的表格列或单元格更改对齐方式。我特别喜欢在合适的情况下利用继承特性。
table {
text-align: left;
}
现在,我们来假设希望在表格单元格之间添加网格线。我们不能简单地为每个表格行添加边框。
/* 这还不能正常工作 */
tr {
border-bottom: 1px solid;
}
如果为每个表格单元格添加边框,可以看到默认情况下单元格之间存在小间隙。
th,
td {
border: 1px solid;
}
这对于创建美观的表格并不是很有用。我们可以将表格的 border-collapse
属性 从默认值 separate
改为 collapse
。这样可以让表格单元格共享边框,使表格看起来更符合预期。
table {
text-align: left;
border-collapse: collapse;
}
这种更改的一个有用的副作用是,我们现在可以为表格行设置边框,而不仅仅是单独的单元格。
设计考虑因素
在定制表格样式时,有一些设计考虑因素需要我们关注。
文本对齐
在从左到右书写的语言(如英语)中,文本通常在左对齐时更容易阅读,而对于从右到左书写的语言,右对齐则更加合适。另一方面,数字数据在某些情况下可能更适合右对齐,因为这样可以更容易地将数值进行对比,或者与总和进行比较。
为了考虑可能存在不同书写方向的语言,我们可以使用 text-align
属性的逻辑值。text-align: start
可以确保当表格(或文档)设置了不同的书写方向时,文本的对齐方式也能保持一致。
将 text-align: start
应用于 <table>
元素在所有浏览器中可能无法生效,但我们可以将其应用于表格标题和表格说明。由于我们表格中的其他单元格包含的是数字值,我们将这些单元格(及其对应的列标题)对齐到右端:
th,
caption {
text-align: start;
}
thead th:not(:first-child),
td {
text-align: end;
}
表头和表脚
行和列的标题已经通过加粗的字体重量来区分,但为了让用户更容易理解这张表格,我们可以通过明确标出表头和表脚来增强可读性。我们可以通过为这些行添加更重的边框,或更改背景颜色(或两者结合)来实现这一点。
我们可以使用逻辑的 border-block
属性仅在块轴上设置边框样式。(在这里,我们还为单元格添加了一些内边距。)
thead {
border-block-end: 2px solid;
background: whitesmoke;
}
tfoot {
border-block: 2px solid;
background: whitesmoke;
}
th,
td {
border: 1px solid lightgrey;
padding: 0.25rem 0.75rem;
}
列之间的边框可能并不总是必要的,因为单元格内容的对齐自然帮助我们辨别列与列之间的关系。不过,对于大型或复杂的表格,边框有时可以提高可读性。我通常倾向于将单元格的边框颜色调整为比表头和表脚颜色更浅,以避免表格显得拥挤。
行列着色
交替着色表格行背景是另一种改善表格可读性的方式,作为边框的替代方法。在这个示例中,我们将一个主题颜色设置为表格的自定义属性,并使用 color-mix()
函数创建一个透明的变体,用于交替着色表格行。
table {
--color: #d0d0f5;
}
thead,
tfoot {
background: var(--color);
}
tbody tr:nth-child(even) {
background: color-mix(in srgb, var(--color), transparent 60%);
}
使用 CSS 样式
除了这些设计考虑因素外,还有一些实际的内容需要考虑,以帮助用户浏览我们的表格。接下来,我们来看一下 CSS 如何帮助我们实现这些功能。
标题位置
也许我们更希望将表格标题放在表格内容之后?我们可以使用 caption-side
属性来实现这一点,它将确保视觉顺序仍然反映表格元素在辅助技术中被感知的顺序。
table {
caption-side: bottom;
}
管理溢出
如果我们在表格中添加更多的列(或者在狭窄的视口中查看),可能会导致网页出现水平溢出。在第二个表格示例中,使用了一个更大的数据集,我们可以更清晰地看到这一点。
为了避免用户需要滚动整个网页才能查看表格的列,我们可以将表格包裹在一个应用了 overflow: auto
或 overflow: scroll
的元素中。
<div class="wrapper">
<table>
...
</table>
</div>
.wrapper {
overflow: scroll;
}
现在用户可以通过滚动表格本身来查看所有数据。然而,这样的体验并不理想,因为他们仍然需要滚动回到开始位置才能查看行标题,且在滚动时可能会丢失表格的定位。
固定行和列
我们可以使用固定定位确保行标题在用户滚动时保持可见。
th:first-child {
position: sticky;
inset-inline-start: 0;
}
提示:
inset-inline-start
是逻辑属性,相当于左至右书写模式下的 left
,因此你也可以使用 left: 0
来实现相同效果。
我们还需要为这些表格单元格设置背景颜色,因为当前只有行的背景被样式化,表格内容会在用户滚动时出现在行标题下面。
tbody th {
background: white;
}
thead th,
tfoot th {
background: whitesmoke;
}
一个稍微不便的副作用是,当表格滚动时,行标题的右侧边框会消失在标题内容下方。我们可以通过一种解决方法来修复这个问题,尽管这种方法感觉有些“hacky”。
首先,我们将使用 border-inline-end
逻辑属性(相当于左至右书写模式中的 border-right
)移除标题单元格的右边框。
th:first-child {
position: sticky;
left: 0;
border-inline-end: none;
}
td:first-of-type,
:where(thead, tfoot) th:nth-child(2) {
border-inline-start: none;
}
在上面的代码中,我们使用了 :where()
伪类来减少代码行数。这个选择器的意思是“选择作为 <thead>
或 <tfoot>
的子元素的第二个 <th>
”。否则,我们可以单独写出两个选择器:
:where(thead, tfoot) th:nth-child(2) {}
/* 等效于:*/
thead th:nth-child(2),
tfoot th:nth-child(2) {}
然后,我们将样式应用到 ::after
伪元素上,将其定位在行标题的右侧边缘。
th:first-child::after {
content: '';
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
width: 1px;
height: 100%;
background: lightgrey;
}
可访问性
Adrian Roselli 提醒我,这个解决方案在目前的状态下对键盘用户和屏幕阅读器用户不可访问。为了改进这一点,我们需要为我们的可滚动区域指定角色,并为屏幕阅读器提供可访问的名称,还需要让其能够通过键盘聚焦。我们可以使用 aria-labelledby,并将表格标题的 ID 作为值。
将包装元素(滚动容器)设置为 tabindex="0"
,满足了后者的需求。
<div class="wrapper" tabindex="0" role="region" aria-labelledby="tableCaption_01">
<table>
<caption id="tableCaption_01">...</caption>
</table>
</div>
我们还应该使用 CSS 提供一个在滚动容器聚焦时的视觉样式。我喜欢 Adrian 使用属性选择器来样式化的解决方案:
[role="region"][aria-labelledby][tabindex]:focus {
outline: .1em solid rgba(0,0,0,.1);
}
正如 Adrian 在他的文章《Under-Engineered Responsive Tables》中提到的:
这个选择器确保表格在 HTML 正确标记并对键盘和屏幕阅读器用户可访问的情况下,不会被剪裁。
Adrian 还建议为滚动条添加阴影作为视觉提示,因为某些浏览器中的滚动条会消失。他建议使用 Lea Verou 的背景附着技术,使用 background-attachment: local
。下面是代码片段,也包含在最终演示中 — 你可以在 Adrian 的文章中查看详细内容。
div[tabindex="0"][aria-labelledby][role="region"] {
background:
linear-gradient(to right, transparent 30%, rgba(255,255,255,0)),
linear-gradient(to right, rgba(255,255,255,0), white 70%) 0 100%,
radial-gradient(farthest-side at 0% 50%, rgba(0,0,0,0.2), rgba(0,0,0,0)),
radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,0.2), rgba(0,0,0,0)) 0 100%;
background-repeat: no-repeat;
background-color: #fff;
background-size: 4em 100%, 4em 100%, 1.4em 100%, 1.4em 100%;
background-position: 0 0, 100%, 0 0, 100%;
background-attachment: local, local, scroll, scroll;
}
现在,用户可以通过滚动表格本身来查看表格中的所有数据。然而,这样的体验并不理想,因为他们仍然需要滚动回到开始位置才能看到行标题,这很麻烦,而且他们很容易在表格中迷失位置。
垂直对齐
当我们的表格列宽度小于内容时,文本会自动换行。默认情况下,单元格内容是垂直居中的。这在某些情况下是可以接受的,但对于某些类型的数据,可能会使表格更难阅读。对于文本内容,将其与基线对齐可能会更合适。
另一方面,列标题可能更适合放置在表格单元格的底部,这样我们就不会让一些标题浮在表格数据的上方。
你选择如何对齐表格内容取决于设计和用户需求,但我们可以使用 vertical-align 属性来控制表格单元格的垂直位置。让我们将列标题对齐到底部,将表格正文单元格对齐到基线。
th,
td {
vertical-align: baseline;
}
thead th {
vertical-align: bottom;
}
列宽
你可能会注意到我们的表格列宽不均匀。这里每一列的宽度由其标题的长度决定,而标题的长度比正文单元格中的内容要长。如果正文列的宽度相等,我们的表格会更容易阅读。
首先,表格的宽度是由内容决定的。如果我们为表格设置一个宽度,表格看起来会好一些,但列宽仍然不均匀。我们可以通过设置每个标题单元格(除了第一个单元格)的宽度来解决这个问题:
thead th:not(:first-child) {
width: 9rem;
}
当视口变窄时,我们仍然会看到列宽不均匀,因此需要更多的控制来设置列宽。
为了更好地控制列宽,我们可以通过 table-layout 属性来改变浏览器用于确定表格布局的算法。
将其改为 fixed
(默认是 auto
)会让浏览器忽略单元格内容,而是使用第一行中的列或单元格上定义的宽度来确定列宽。
提示:如果这听起来有点复杂,CSS Tricks 详细讲解了这个概念,并提供了很多实用的演示。
为了使 fixed
布局在现代浏览器中生效,我们还需要为表格设置 width 属性。这里我们使用了 max() 函数,告诉浏览器选择两个参数中较大的一个值 — 65rem 或 100%。这意味着,在较小的视口下,我们的表格宽度至少为 65rem(因为它是可滚动的),而在较大的视口下,它会占据可用的整个宽度。
table {
width: max(65rem, 100%);
table-layout: fixed;
}
现在,我们的表格列应该都有相等的宽度了,但我们可能希望第一个列(包含行标题)有一个不同的宽度。如果我们为第一个
th:first-of-type {
width: 10rem;
}
参考链接
- Styling Tables the Modern CSS Way
- \
: The Table element