Typst 表格怎么做:三线表、列宽、合并单元格与 CSV 数据
Typst 表格的常用写法:table 的 columns 与 auto、fr、固定列宽,align 对齐,stroke 与 table.hline 画三线表,figure 加表题和 @tab 引用,table.cell 合并单元格,跨页长表格与重复表头,隔行填色,以及用 csv() 从文件读取数据,每个示例都能直接打开编译。
最后核对:2026-10-24Typst 的表格和 LaTeX 一样分两层:table 排出表格本身,figure 给它加标题和编号。不同的是,列格式、线条、对齐、填充都是 table(...) 的参数,不需要额外的宏包。本文和 LaTeX 表格一文的内容一一对应,可以对照着看。在 JointTex 里,Typst 项目边输入边编译,不用点编译按钮;点 PDF 里的表格能跳到对应的源码,反过来也可以。
最基本的表格怎么写
// title: Typst 表格入门
#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), lang: "zh", region: "cn")
#set heading(numbering: "1.")
= 最简单的表格
// columns 写一个整数,表示有几列;单元格按顺序从左到右、从上到下填入
// 默认每个单元格四周都有 1pt 的黑色线条
#table(
columns: 3,
[姓名], [性别], [成绩],
[张三], [男], [92],
[李四], [女], [88],
)
= 列宽和对齐
// columns 写成数组时,每一项是一列的宽度:
// auto 按内容定宽,1fr 和 2fr 按 1:2 分掉剩余宽度,3cm 是固定宽度
// align 也可以写成数组,一列一个对齐方式
#table(
columns: (auto, 1fr, 2fr, 3cm),
align: (center, left, left, right),
[编号], [名称], [说明], [数值],
[1], [甲], [按 1fr 分到三分之一的剩余宽度], [10.5],
[2], [乙], [按 2fr 分到三分之二,文字太长时在列内自动换行], [123.25],
)
= 带标题、可引用的表格
// 表题放在表格上方:只对 kind 为 table 的 figure 生效
#show figure.where(kind: table): set figure.caption(position: top)
如@tab:score 所示,两位同学的成绩都在 85 分以上。
// figure 负责标题和编号,table 负责表格本身;标签写在 figure 后面
#figure(
table(
columns: 3,
[姓名], [数学], [英语],
[张三], [92], [85],
[李四], [88], [90],
),
caption: [期末成绩],
) <tab:score>
table 里带名字的参数(columns:、align: 等)设置格式,不带名字的参数就是单元格,从左到右、从上到下依次填入。
- 每个单元格是一个内容块
[...],单元格之间用逗号分隔。不用\\结束一行,填满columns指定的列数就自动换行。 columns: 3表示三列,宽度由内容决定。写成数组时,数组里每一项是一列的宽度:auto按内容定宽;1fr、2fr按比例分掉剩下的宽度(fr是 fraction,份数);3cm、20%是固定宽度。fr列和 LaTeX 的p{...}列、tabularx的X列作用相同:文字超出列宽时在列内自动换行。只要有一列是fr,表格就正好占满整行,不会伸出页边。align写一个值对所有单元格生效,写成数组则一列一个:left、center、right,还能和垂直方向组合,如center + horizon(水平、垂直都居中)。
默认情况下,每个单元格四周都有 1pt 的黑线,内边距是 5pt。
表格标题和引用怎么加
把 table 放进 figure,用 caption: 写标题,在 figure(...) 的右括号后面写标签 <tab:score>,正文里用 @tab:score 引用。
-
Typst 默认把标题放在下方,图和表都一样。中文论文的习惯是表题在表格上方,所以在文档开头加一行:
#show figure.where(kind: table): set figure.caption(position: top)figure.where(kind: table)只选中装着表格的figure,图片的标题仍在下方。 -
figure看得出里面是表格,表格和图片分开编号。 -
文档设置了
#set text(lang: "zh")时,表题前缀是"表",编号和标题之间空一个字宽(em 空格);引用@tab:score得到"表 1"。不设lang时是英文的"Table 1: 期末成绩",引用得到"Table 1"。 -
标签必须贴在
figure上。把标签直接写在table后面再引用,会报错:error: cannot reference table directly, try putting it into a figure -
标签名不存在时,报
error: label `<tab:none>` does not exist in the document。 -
@后面要留空格,或者紧跟中文标点(如",""。")。 标签名里可以有汉字,如@tab:score所示会被读成引用<tab:score所示>,报上面那个"不存在"的错误。写成如@tab:score 所示,这个空格排出来就是数字和汉字之间正常的间距。 -
@前面不要加空格。正文 @tab:score里的空格会原样排出来。
和 LaTeX 不同,figure 默认不浮动,表格就排在源码所在的位置。需要浮动时给 figure 加 placement 参数,见 Typst 插图。
论文里的三线表怎么做
先用 stroke: none 去掉默认的全部线条,再用 table.hline 画横线:它写在哪两行单元格之间,线就画在哪里,stroke 参数控制粗细。
// title: Typst 三线表
#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), lang: "zh", region: "cn")
// 表题放在表格上方
#show figure.where(kind: table): set figure.caption(position: top)
@tab:result 是论文里常见的三线表:顶线和底线粗,栏目线细,没有竖线。
#figure(
table(
columns: 4,
// 去掉默认的全部线条,下面只画需要的三条
stroke: none,
// 第一列左对齐,其余列居中
align: (left, center, center, center),
table.hline(stroke: 1.5pt), // 顶线
// table.header 标出表头:表格跨页时,表头会在新的一页重复
table.header([方法], [准确率(%)], [召回率(%)], [耗时(s)]),
table.hline(stroke: 0.75pt), // 栏目线
[方法 A], [91.2], [88.5], [12.3],
[方法 B], [93.6], [90.1], [15.8],
// 中文字体有真正的粗体(Noto Serif CJK SC Bold),*…* 可以直接加粗
[本文方法], [*95.4*], [*92.7*], [13.1],
table.hline(stroke: 1.5pt), // 底线
),
caption: [不同方法的实验结果],
) <tab:result>
// 分组表头:table.cell(colspan: 2) 横跨两列,
// table.hline(start: 1, end: 3) 只在第 2、3 列下面画线(列号从 0 开始,end 不含)
#figure(
table(
columns: 5,
stroke: none,
align: center,
// 第 3、4 列之间留 1em 空隙,两条分组细线就不会连成一条
column-gutter: (0pt, 0pt, 1em, 0pt),
table.hline(stroke: 1.5pt),
table.header(
[], table.cell(colspan: 2)[训练集], table.cell(colspan: 2)[测试集],
table.hline(start: 1, end: 3, stroke: 0.5pt),
table.hline(start: 3, end: 5, stroke: 0.5pt),
[模型], [准确率], [损失], [准确率], [损失],
),
table.hline(stroke: 0.75pt),
[基线], [0.92], [0.21], [0.88], [0.30],
[改进], [0.95], [0.15], [0.91], [0.24],
table.hline(stroke: 1.5pt),
),
caption: [分组表头],
) <tab:group>
@tab:group 的两组表头下面各有一条细线。
几个要点:
- 顶线、底线用粗一些的线(示例用 1.5pt),表头下面的栏目线用细线(0.75pt)。
table.header(...)把表头的单元格包起来。它不改变外观,但表格跨页时,Typst 会在新的一页重复表头(见下文)。- 只在部分列下面画线用
table.hline(start: 1, end: 3),相当于 booktabs 的\cmidrule{2-3}:列号从 0 开始,含start、不含end,这里画在第 2、3 列下面。 - 两组分组线紧挨着时会连成一条。
hline没有\cmidrule(lr)那样的两端缩进,示例用column-gutter: (0pt, 0pt, 1em, 0pt)在第 3、4 列之间留出空隙:贯穿整行的线跨过空隙,部分列下面的线不跨,两条分组线就断开了。 - 单位、百分号直接写在表头里。Typst 的标记里
%不是特殊字符,不需要转义。
线条和底色还能怎么设置
stroke写成字典:stroke: (x: none, y: 0.5pt)去掉竖线、保留横线,x指竖线,y指横线。stroke和fill都可以写成函数,参数是单元格的列号x和行号y(从 0 开始)。fill: (x, y) => if calc.even(y) { luma(235) }让偶数行填浅灰色,就是隔行填色(斑马纹),见下文的长表格示例;函数没有返回值的行不填色。- 单个单元格用
table.cell(fill: ..., stroke: ...)单独设置。
怎么合并单元格
- 跨列:
table.cell(colspan: 2)[成绩],相当于 LaTeX 的\multicolumn。 - 跨行:
table.cell(rowspan: 2)[类别],相当于multirow宏包的\multirow,不需要加载任何东西。 - 被合并占用的位置不用再写任何东西,也不用留空的单元格。这一点和 LaTeX 相反:LaTeX 里被
\multirow占用的格子要写空,&不能少;Typst 里多写一个空格子,后面的内容就会全部错后一格。 table.cell也可以单独改某一格的对齐方式,如table.cell(colspan: 2, align: right)[平均]。- 合并后超出列数会报错:
error: cell's colspan would cause it to exceed the available column(s)。
// title: Typst 合并单元格
#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), lang: "zh", region: "cn")
#show figure.where(kind: table): set figure.caption(position: top)
// table.cell(colspan: n) 横跨 n 列,table.cell(rowspan: n) 纵跨 n 行。
// 被合并占用的位置不用再写内容,后面的单元格会自动跳过它们。
#figure(
table(
columns: 4,
align: center + horizon, // 水平居中,垂直方向也居中
table.header(
table.cell(rowspan: 2)[类别],
table.cell(rowspan: 2)[样本],
table.cell(colspan: 2)[成绩],
// 第二行表头只剩“成绩”下面的两格
[期中], [期末],
),
table.cell(rowspan: 2)[甲组], [1], [85], [88],
// 上一行的“甲组”占了这一行的第一格,所以这里只写三格
[2], [90], [92],
table.cell(rowspan: 2)[乙组], [3], [78], [80],
[4], [82], [86],
// 跨列的单元格也可以单独改对齐方式
table.cell(colspan: 2, align: right)[平均], [83.75], [86.5],
),
caption: [分组成绩],
) <tab:merge>
@tab:merge 中,类别和样本两格各纵跨两行,成绩一格横跨两列。
表格太长、要跨页怎么办
table 本身能跨页,但放进 figure 后就不能了:figure 默认是一个不能拆开的整体,当前页放不下时,会整个挪到下一页,并在原处留下空白;比一整页还长时,会伸出页面底部。让表格的 figure 可以拆开:
#show figure.where(kind: table): set block(breakable: true)
// title: Typst 跨页长表格
#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), lang: "zh", region: "cn")
// 页面设得小一些,表格很快就会跨页
#set page(paper: "a5")
#show figure.where(kind: table): set figure.caption(position: top)
// figure 默认是一个不能拆开的整体:放不下时会整个挪到下一页。
// 让表格的 figure 可以跨页:
#show figure.where(kind: table): set block(breakable: true)
@tab:long 共 40 行,表头在每一页的顶部重复,隔行填充浅灰色。
#figure(
table(
columns: (auto, 1fr, auto),
stroke: none,
align: (center, left, right),
// fill 写成函数,参数是列号 x 和行号 y(都从 0 开始);
// 表头是第 0 行,数据从第 1 行开始,偶数行填浅灰色
fill: (x, y) => if y > 0 and calc.even(y) { luma(235) },
// table.header 默认 repeat: true,跨页后在新一页重复
table.header(
table.hline(stroke: 1.5pt),
[编号], [名称], [数值],
table.hline(stroke: 0.75pt),
),
// 用循环生成 40 行数据:每次产生三个单元格
..for i in range(1, 41) {
([#i], [样品 #i], [#(i * 12.5)])
},
table.hline(stroke: 1.5pt),
),
caption: [长表格示例],
) <tab:long>
- 用
table.header包起来的表头会在每一页的顶部重复,写在它里面的table.hline也一起重复,所以续页也有顶线。不想重复时写table.header(repeat: false, ...)。 - 放在表格最后的
table.hline不只画在最后一页:表格在每一页断开的地方都会画出这条线,每一页的表格都是封闭的三线表。 - 需要每一页底部都出现的内容(如合计行),可以用
table.footer(...),它同样默认在每一页重复。
不需要像 LaTeX 那样换用 longtable 环境,也不需要多编译一次。
表格数据能不能从文件读
能。csv("data.csv") 读入项目里的 CSV 文件,得到一个数组:每一项是一行,每一行又是一个字符串数组。改了数据,表格跟着变。
// title: Typst 从 CSV 读取表格
#set text(font: ("Libertinus Serif", "Noto Serif CJK SC"), lang: "zh", region: "cn")
#show figure.where(kind: table): set figure.caption(position: top)
// csv() 读入项目里的 data.csv,得到一个数组,每一项是一行,
// 每一行又是一个字符串数组。第一行是表头。
#let rows = csv("data.csv")
#let head = rows.first() // 第一行:表头
#let body = rows.slice(1) // 其余各行:数据
@tab:sample 的数据直接来自 data.csv,改动 CSV 文件后表格跟着变。
#figure(
table(
// 列数取表头的长度,不必手写
columns: head.len(),
stroke: none,
align: (left, right, right),
table.hline(stroke: 1.5pt),
// .. 把数组展开成多个参数;map 把字符串变成单元格内容
table.header(..head.map(h => [#h])),
table.hline(stroke: 0.75pt),
// flatten 把“行的数组”摊平成一串单元格
..body.flatten(),
table.hline(stroke: 1.5pt),
),
caption: [各样品的测量结果],
) <tab:sample>
-
rows.first()取第一行做表头,rows.slice(1)取其余各行,.flatten()把"行的数组"摊平成一串单元格交给table。 -
用分号分隔的文件写
csv("data.csv", delimiter: ";")。 -
文件必须是 UTF-8 编码。在中文版 Excel 里直接"另存为 CSV",得到的常常是 GBK 编码,会报:
error: failed to parse CSV (file is not valid UTF-8 in data.csv:1:1)另存时选择"CSV UTF-8"格式即可。
-
某一行的列数和第一行不同时,报错会指出行号:
error: failed to parse CSV (found 3 instead of 2 fields in line 2)。
字号、内边距和表题格式怎么改
- 表格字号小一号:
#show table: set text(size: 9pt),对它后面的所有表格生效。只改一个表格时,把这一行和那个表格一起放进#[ ... ],规则只在这一块里起作用。 - 行距太挤或太松:调单元格的内边距,如
#set table(inset: (x: 8pt, y: 4pt)),x是左右,y是上下。 - 表头加粗:
#show table.cell.where(y: 0): set text(weight: "bold")。Typst 不会给没有粗体字形的中文字体"伪造"粗体;示例用的 Noto Serif CJK SC 有粗体,能正常加粗。 - 表题改成"表 1:标题"、字号小一号:
#set figure.caption(separator: [:])和#show figure.caption: set text(size: 9pt),对图题和表题都生效。只改表题时,在show figure.where(kind: table):后面写set figure.caption(separator: [:])。
常见错误
- 单元格之间漏了逗号:
[a] [b]报error: expected comma。 - 单元格个数不是列数的整数倍:不报错,最后一行缺的格子留空,表格错位时先数一数单元格。
- 单元格里的
//://在 Typst 里是注释,[a // b]会把这一行后面的内容(包括])都当成注释,报error: unclosed delimiter。要写两个斜杠时用\/\/。以https://开头的网址不受影响,Typst 会把它识别为链接。 - 单元格里的
@:邮箱[email protected]会被当成引用,报error: label `<b.com>` does not exist in the document,写成a\@b.com。
报错在 JointTex 里会在源码对应位置画上波浪线,并列在"问题"标签页中。更多报错见 Typst 常见报错。表格里的中文字体设置见 Typst 中文排版。
本文示例只用了编辑器页面自带的字体(Libertinus Serif 和 Noto Serif CJK SC)。项目在 Server 上编译时还有 Noto Sans CJK 等其他中文字体,也可以把字体文件上传到项目的 fonts/ 文件夹。每个示例都可以在 JointTex 中一键打开为新项目,改成自己的表格。
