File size: 52,972 Bytes
2d83f30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
---
description: |
  Not sure how to change table strokes? Need to rotate a table? This guide
  explains all you need to know about tables in Typst.
---

# Table guide
Tables are a great way to present data to your readers in an easily readable,
compact, and organized manner. They are not only used for numerical values, but
also survey responses, task planning, schedules, and more. Because of this wide
set of possible applications, there is no single best way to lay out a table.
Instead, think about the data you want to highlight, your document's overarching
design, and ultimately how your table can best serve your readers.

Typst can help you with your tables by automating styling, importing data from
other applications, and more! This guide takes you through a few of the most
common questions you may have when adding a table to your document with Typst.
Feel free to skip to the section most relevant to you – we designed this guide
to be read out of order.

If you want to look up a detail of how tables work, you should also [check out
their reference page]($table). And if you are looking for a table of contents
rather than a normal table, the reference page of the [`outline`
function]($outline) is the right place to learn more.

## How to create a basic table? { #basic-tables }
In order to create a table in Typst, use the [`table` function]($table). For a
basic table, you need to tell the table function two things:

- The number of columns
- The content for each of the table cells

So, let's say you want to create a table with two columns describing the
ingredients for a cookie recipe:

```example
#table(
  columns: 2,
  [*Amount*], [*Ingredient*],
  [360g], [Baking flour],
  [250g], [Butter (room temp.)],
  [150g], [Brown sugar],
  [100g], [Cane sugar],
  [100g], [70% cocoa chocolate],
  [100g], [35-40% cocoa chocolate],
  [2], [Eggs],
  [Pinch], [Salt],
  [Drizzle], [Vanilla extract],
)
```

This example shows how to call, configure, and populate a table. Both the column
count and cell contents are passed to the table as arguments. The [argument
list]($function) is surrounded by round parentheses. In it, we first pass the
column count as a named argument. Then, we pass multiple [content
blocks]($content) as positional arguments. Each content block contains the
contents for a single cell.

To make the example more legible, we have placed two content block arguments on
each line, mimicking how they would appear in the table. You could also write
each cell on its own line. Typst does not care on which line you place the
arguments. Instead, Typst will place the content cells from left to right (or
right to left, if that is the writing direction of your language) and then from
top to bottom. It will automatically add enough rows to your table so that it
fits all of your content.

It is best to wrap the header row of your table in the [`table.header`
function]($table.header). This clarifies your intent and will also allow future
versions of Typst to make the output more accessible to users with a screen
reader:

```example
#table(
  columns: 2,
  table.header[*Amount*][*Ingredient*],
  [360g], [Baking flour],
<<<  // ... the remaining cells
>>>  [250g], [Butter (room temp.)],
>>>  [150g], [Brown sugar],
>>>  [100g], [Cane sugar],
>>>  [100g], [70% cocoa chocolate],
>>>  [100g], [35-40% cocoa chocolate],
>>>  [2], [Eggs],
>>>  [Pinch], [Salt],
>>>  [Drizzle], [Vanilla extract],
)
```

You could also write a show rule that automatically [strongly
emphasizes]($strong) the contents of the first cells for all tables. This
quickly becomes useful if your document contains multiple tables!

```example
#show table.cell.where(y: 0): strong

#table(
  columns: 2,
  table.header[Amount][Ingredient],
  [360g], [Baking flour],
<<<  // ... the remaining cells
>>>  [250g], [Butter (room temp.)],
>>>  [150g], [Brown sugar],
>>>  [100g], [Cane sugar],
>>>  [100g], [70% cocoa chocolate],
>>>  [100g], [35-40% cocoa chocolate],
>>>  [2], [Eggs],
>>>  [Pinch], [Salt],
>>>  [Drizzle], [Vanilla extract],
)
```

We are using a show rule with a selector for cell coordinates here instead of
applying our styles directly to `table.header`. This is due to a current
limitation of Typst that will be fixed in a future release.

Congratulations, you have created your first table! Now you can proceed to
[change column sizes](#column-sizes), [adjust the strokes](#strokes), [add
striped rows](#fills), and more!

## How to change the column sizes? { #column-sizes }
If you create a table and specify the number of columns, Typst will make each
column large enough to fit its largest cell. Often, you want something
different, for example, to make a table span the whole width of the page. You
can provide a list, specifying how wide you want each column to be, through the
`columns` argument. There are a few different ways to specify column widths:

- First, there is `{auto}`. This is the default behavior and tells Typst to grow
  the column to fit its contents. If there is not enough space, Typst will try
  its best to distribute the space among the `{auto}`-sized columns.
- [Lengths]($length) like `{6cm}`, `{0.7in}`, or `{120pt}`. As usual, you can
  also use the font-dependent `em` unit. This is a multiple of your current font
  size. It's useful if you want to size your table so that it always fits
  about the same amount of text, independent of font size.
- A [ratio in percent]($ratio) such as `{40%}`. This will make the column take
  up 40% of the total horizontal space available to the table, so either the
  inner width of the page or the table's container. You can also mix ratios and
  lengths into [relative lengths]($relative). Be mindful that even if you
  specify a list of column widths that sum up to 100%, your table could still
  become larger than its container. This is because there can be
  [gutter]($table.gutter) between columns that is not included in the column
  widths. If you want to make a table fill the page, the next option is often
  very useful.
- A [fractional part of the free space]($fraction) using the `fr` unit, such as
  `1fr`. This unit allows you to distribute the available space to columns. It
  works as follows: First, Typst sums up the lengths of all columns that do not
  use `fr`s. Then, it determines how much horizontal space is left. This
  horizontal space then gets distributed to all columns denominated in `fr`s.
  During this process, a `2fr` column will become twice as wide as a `1fr`
  column. This is where the name comes from: The width of the column is its
  fraction of the total fractionally sized columns.

Let's put this to use with a table that contains the dates, numbers, and
descriptions of some routine checks. The first two columns are `auto`-sized and
the last column is `1fr` wide as to fill the whole page.

```example
#table(
  columns: (auto, auto, 1fr),
  table.header[Date][°No][Description],
  [24/01/03], [813], [Filtered participant pool],
  [24/01/03], [477], [Transitioned to sec. regimen],
  [24/01/11], [051], [Cycled treatment substrate],
)
```

Here, we have passed our list of column lengths as an [array], enclosed in round
parentheses, with its elements separated by commas. The first two columns are
automatically sized, so that they take on the size of their content and the
third column is sized as `{1fr}` so that it fills up the remainder of the space
on the page. If you wanted to instead change the second column to be a bit more
spacious, you could replace its entry in the `columns` array with a value like
`{6em}`.

## How to caption and reference my table? { #captions-and-references }
A table is just as valuable as the information your readers draw from it. You
can enhance the effectiveness of both your prose and your table by making a
clear connection between the two with a cross-reference. Typst can help you with
automatic [references]($ref) and the [`figure` function]($figure).

Just like with images, wrapping a table in the `figure` function allows you to
add a caption and a label, so you can reference the figure elsewhere. Wrapping
your table in a figure also lets you use the figure's `placement` parameter to
float it to the top or bottom of a page.

Let's take a look at a captioned table and how to reference it in prose:

```example
>>> #set page(width: 14cm)
#show table.cell.where(y: 0): set text(weight: "bold")

#figure(
  table(
    columns: 4,
    stroke: none,

    table.header[Test Item][Specification][Test Result][Compliance],
    [Voltage], [220V ± 5%], [218V], [Pass],
    [Current], [5A ± 0.5A], [4.2A], [Fail],
  ),
  caption: [Probe results for design A],
) <probe-a>

The results from @probe-a show that the design is not yet optimal.
We will show how its performance can be improved in this section.
```

The example shows how to wrap a table in a figure, set a caption and a label,
and how to reference that label. We start by using the `figure` function. It
expects the contents of the figure as a positional argument. We just put the
table function call in its argument list, omitting the `#` character because it
is only needed when calling a function in markup mode. We also add the caption
as a named argument (above or below) the table.

After the figure call, we put a label in angle brackets (`[<probe-a>]`). This
tells Typst to remember this element and make it referenceable under this name
throughout your document. We can then reference it in prose by using the at sign
and the label name `[@probe-a]`. Typst will print a nicely formatted reference
and automatically update the label if the table's number changes.

## How to get a striped table? { #fills }
Many tables use striped rows or columns instead of strokes to differentiate
between rows and columns. This effect is often called _zebra stripes._ Tables
with zebra stripes are popular in Business and commercial Data Analytics
applications, while academic applications tend to use strokes instead.

To add zebra stripes to a table, we use the `table` function's `fill` argument.
It can take three kinds of arguments:

- A single color (this can also be a gradient or a tiling) to fill all cells
  with. Because we want some cells to have another color, this is not useful if
  we want to build zebra tables.
- An array with colors which Typst cycles through for each column. We can use an
  array with two elements to get striped columns.
- A function that takes the horizontal coordinate `x` and the vertical
  coordinate `y` of a cell and returns its fill. We can use this to create
  horizontal stripes or [checkerboard patterns]($grid.cell).

Let's start with an example of a horizontally striped table:

```example
>>> #set page(width: 16cm)
#set text(font: "IBM Plex Sans")

// Medium bold table header.
#show table.cell.where(y: 0): set text(weight: "medium")

// Bold titles.
#show table.cell.where(x: 1): set text(weight: "bold")

// See the strokes section for details on this!
#let frame(stroke) = (x, y) => (
  left: if x > 0 { 0pt } else { stroke },
  right: stroke,
  top: if y < 2 { stroke } else { 0pt },
  bottom: stroke,
)

#set table(
  fill: (rgb("EAF2F5"), none),
  stroke: frame(rgb("21222C")),
)

#table(
  columns: (0.4fr, 1fr, 1fr, 1fr),

  table.header[Month][Title][Author][Genre],
  [January], [The Great Gatsby], [F. Scott Fitzgerald], [Classic],
  [February], [To Kill a Mockingbird], [Harper Lee], [Drama],
  [March], [1984], [George Orwell], [Dystopian],
  [April], [The Catcher in the Rye], [J.D. Salinger], [Coming-of-Age],
)
```

This example shows a book club reading list. The line `{fill: (rgb("EAF2F5"),
 none)}` in `table`'s set rule is all that is needed to add striped columns. It
tells Typst to alternate between coloring columns with a light blue (in the
[`rgb`]($color.rgb) function call) and nothing (`{none}`). Note that we
extracted all of our styling from the `table` function call itself into set and
show rules, so that we can automatically reuse it for multiple tables.

Because setting the stripes itself is easy we also added some other styles to
make it look nice. The other code in the example provides a dark blue
[stroke](#stroke-functions) around the table and below the first line and
emboldens the first row and the column with the book title. See the
[strokes](#strokes) section for details on how we achieved this stroke
configuration.

Let's next take a look at how we can change only the set rule to achieve
horizontal stripes instead:

```example
>>> #set page(width: 16cm)
>>> #set text(font: "IBM Plex Sans")
>>> #show table.cell.where(x: 1): set text(weight: "medium")
>>> #show table.cell.where(y: 0): set text(weight: "bold")
>>>
>>> #let frame(stroke) = (x, y) => (
>>>   left: if x > 0 { 0pt } else { stroke },
>>>   right: stroke,
>>>   top: if y < 2 { stroke } else { 0pt },
>>>   bottom: stroke,
>>> )
>>>
#set table(
  fill: (_, y) => if calc.odd(y) { rgb("EAF2F5") },
  stroke: frame(rgb("21222C")),
)
>>>
>>> #table(
>>>   columns: (0.4fr, 1fr, 1fr, 1fr),
>>>
>>>   table.header[Month][Title][Author][Genre],
>>>   [January], [The Great Gatsby],
>>>     [F. Scott Fitzgerald], [Classic],
>>>   [February], [To Kill a Mockingbird],
>>>     [Harper Lee], [Drama],
>>>   [March], [1984],
>>>     [George Orwell], [Dystopian],
>>>   [April], [The Catcher in the Rye],
>>>     [J.D. Salinger], [Coming-of-Age],
>>> )
```

We just need to replace the set rule from the previous example with this one and
get horizontal stripes instead. Here, we are passing a function to `fill`. It
discards the horizontal coordinate with an underscore and then checks if the
vertical coordinate `y` of the cell is odd. If so, the cell gets a light blue
fill, otherwise, no fill is returned.

Of course, you can make this function arbitrarily complex. For example, if you
want to stripe the rows with a light and darker shade of blue, you could do
something like this:

```example
>>> #set page(width: 16cm)
>>> #set text(font: "IBM Plex Sans")
>>> #show table.cell.where(x: 1): set text(weight: "medium")
>>> #show table.cell.where(y: 0): set text(weight: "bold")
>>>
>>> #let frame(stroke) = (x, y) => (
>>>   left: if x > 0 { 0pt } else { stroke },
>>>   right: stroke,
>>>   top: if y < 2 { stroke } else { 0pt },
>>>   bottom: stroke,
>>> )
>>>
#set table(
  fill: (_, y) => (none, rgb("EAF2F5"), rgb("DDEAEF")).at(calc.rem(y, 3)),
  stroke: frame(rgb("21222C")),
)
>>>
>>> #table(
>>>   columns: (0.4fr, 1fr, 1fr, 1fr),
>>>
>>>   table.header[Month][Title][Author][Genre],
>>>   [January], [The Great Gatsby],
>>>     [F. Scott Fitzgerald], [Classic],
>>>   [February], [To Kill a Mockingbird],
>>>     [Harper Lee], [Drama],
>>>   [March], [1984],
>>>     [George Orwell], [Dystopian],
>>>   [April], [The Catcher in the Rye],
>>>     [J.D. Salinger], [Coming-of-Age],
>>> )
```

This example shows an alternative approach to write our fill function. The
function uses an array with three colors and then cycles between its values for
each row by indexing the array with the remainder of `y` divided by 3.

Finally, here is a bonus example that uses the _stroke_ to achieve striped rows:

```example
>>> #set page(width: 16cm)
>>> #set text(font: "IBM Plex Sans")
>>> #show table.cell.where(x: 1): set text(weight: "medium")
>>> #show table.cell.where(y: 0): set text(weight: "bold")
>>>
>>> #let frame(stroke) = (x, y) => (
>>>   left: if x > 0 { 0pt } else { stroke },
>>>   right: stroke,
>>>   top: if y < 2 { stroke } else { 0pt },
>>>   bottom: stroke,
>>> )
>>>
#set table(
  stroke: (x, y) => (
    y: 1pt,
    left: if x > 0 { 0pt } else if calc.even(y) { 1pt },
    right: if calc.even(y) { 1pt },
  ),
)
>>>
>>> #table(
>>>   columns: (0.4fr, 1fr, 1fr, 1fr),
>>>
>>>   table.header[Month][Title][Author][Genre],
>>>   [January], [The Great Gatsby],
>>>     [F. Scott Fitzgerald], [Classic],
>>>   [February], [To Kill a Mockingbird],
>>>     [Harper Lee], [Drama],
>>>   [March], [1984],
>>>     [George Orwell], [Dystopian],
>>>   [April], [The Catcher in the Rye],
>>>     [J.D. Salinger], [Coming-of-Age],
>>> )
```

### Manually overriding a cell's fill color { #fill-override }
Sometimes, the fill of a cell needs not to vary based on its position in the
table, but rather based on its contents. We can use the [`table.cell`
element]($table.cell) in the `table`'s parameter list to wrap a cell's content
and override its fill.

For example, here is a list of all German presidents, with the cell borders
colored in the color of their party.

```example
>>> #set page(width: 10cm)
#set text(font: "Roboto")

#let cdu(name) = ([CDU], table.cell(fill: black, text(fill: white, name)))
#let spd(name) = ([SPD], table.cell(fill: red, text(fill: white, name)))
#let fdp(name) = ([FDP], table.cell(fill: yellow, name))

#table(
  columns: (auto, auto, 1fr),
  stroke: (x: none),

  table.header[Tenure][Party][President],
  [1949-1959], ..fdp[Theodor Heuss],
  [1959-1969], ..cdu[Heinrich Lübke],
  [1969-1974], ..spd[Gustav Heinemann],
  [1974-1979], ..fdp[Walter Scheel],
  [1979-1984], ..cdu[Karl Carstens],
  [1984-1994], ..cdu[Richard von Weizsäcker],
  [1994-1999], ..cdu[Roman Herzog],
  [1999-2004], ..spd[Johannes Rau],
  [2004-2010], ..cdu[Horst Köhler],
  [2010-2012], ..cdu[Christian Wulff],
  [2012-2017], [n/a], [Joachim Gauck],
  [2017-],     ..spd[Frank-Walter-Steinmeier],
)
```

In this example, we make use of variables because there only have been a total
of three parties whose members have become president (and one unaffiliated
president). Their colors will repeat multiple times, so we store a function that
produces an array with their party's name and a table cell with that party's
color and the president's name (`cdu`, `spd`, and `fdp`). We then use these
functions in the `table` argument list instead of directly adding the name. We
use the [spread operator]($arguments/#spreading) `..` to turn the items of the
arrays into single cells. We could also write something like
`{[FDP], table.cell(fill: yellow)[Theodor Heuss]}` for each cell directly in the
`table`'s argument list, but that becomes unreadable, especially for the parties
whose colors are dark so that they require white text. We also delete vertical
strokes and set the font to Roboto.

The party column and the cell color in this example communicate redundant
information on purpose: Communicating important data using color only is a bad
accessibility practice. It disadvantages users with vision impairment and is in
violation of universal access standards, such as the
[WCAG 2.1 Success Criterion 1.4.1](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html).
To improve this table, we added a column printing the party name. Alternatively,
you could have made sure to choose a color-blindness friendly palette and mark
up your cells with an additional label that screen readers can read out loud.
The latter feature is not currently supported by Typst, but will be added in a
future release. You can check how colors look for color-blind readers with
[this Chrome extension](https://chromewebstore.google.com/detail/colorblindly/floniaahmccleoclneebhhmnjgdfijgg),
[Photoshop](https://helpx.adobe.com/photoshop/using/proofing-colors.html), or
[GIMP](https://docs.gimp.org/2.10/en/gimp-display-filter-dialog.html).

## How to adjust the lines in a table? { #strokes }
By default, Typst adds strokes between each row and column of a table. You can
adjust these strokes in a variety of ways. Which one is the most practical,
depends on the modification you want to make and your intent:

- Do you want to style all tables in your document, irrespective of their size
  and content? Use the `table` function's [stroke]($table.stroke) argument in a
  set rule.
- Do you want to customize all lines in a single table? Use the `table`
  function's [stroke]($table.stroke) argument when calling the table function.
- Do you want to change, add, or remove the stroke around a single cell? Use the
  `table.cell` element in the argument list of your table call.
- Do you want to change, add, or remove a single horizontal or vertical stroke
  in a single table? Use the [`table.hline`] and [`table.vline`] elements in the
  argument list of your table call.

We will go over all of these options with examples next! First, we will tackle
the `table` function's [stroke]($table.stroke) argument. Here, you can adjust
both how the table's lines get drawn and configure which lines are drawn at all.

Let's start by modifying the color and thickness of the stroke:

```example
#table(
  columns: 4,
  stroke: 0.5pt + rgb("666675"),
  [*Monday*], [11.5], [13.0], [4.0],
  [*Tuesday*], [8.0], [14.5], [5.0],
  [*Wednesday*], [9.0], [18.5], [13.0],
)
```

This makes the table lines a bit less wide and uses a bluish gray. You can see
that we added a width in point to a color to achieve our customized stroke. This
addition yields a value of the [stroke type]($stroke). Alternatively, you can
use the dictionary representation for strokes which allows you to access
advanced features such as dashed lines.

The previous example showed how to use the stroke argument in the table
function's invocation. Alternatively, you can specify the stroke argument in the
`table`'s set rule. This will have exactly the same effect on all subsequent
`table` calls as if the stroke argument was specified in the argument list. This
is useful if you are writing a template or want to style your whole document.

```typ
// Renders the exact same as the last example
#set table(stroke: 0.5pt + rgb("666675"))

#table(
  columns: 4,
  [*Monday*], [11.5], [13.0], [4.0],
  [*Tuesday*], [8.0], [14.5], [5.0],
  [*Wednesday*], [9.0], [18.5], [13.0],
)
```

For small tables, you sometimes want to suppress all strokes because they add
too much visual noise. To do this, just set the stroke argument to `{none}`:

```example
#table(
  columns: 4,
  stroke: none,
  [*Monday*], [11.5], [13.0], [4.0],
  [*Tuesday*], [8.0], [14.5], [5.0],
  [*Wednesday*], [9.0], [18.5], [13.0],
)
```

If you want more fine-grained control of where lines get placed in your table,
you can also pass a dictionary with the keys `top`, `left`, `right`, `bottom`
(controlling the respective cell sides), `x`, `y` (controlling vertical and
horizontal strokes), and `rest` (covers all strokes not styled by other
dictionary entries). All keys are optional; omitted keys will be treated as if
their value was the default value. For example, to get a table with only
horizontal lines, you can do this:

```example
#table(
  columns: 2,
  stroke: (x: none),
  align: horizon,
  [☒], [Close cabin door],
  [☐], [Start engines],
  [☐], [Radio tower],
  [☐], [Push back],
)
```

This turns off all vertical strokes and leaves the horizontal strokes in place.
To achieve the reverse effect (only horizontal strokes), set the stroke argument
to `{(y: none)}` instead.

[Further down in the guide](#stroke-functions), we cover how to use a function
in the stroke argument to customize all strokes individually. This is how you
achieve more complex stroking patterns.

### Adding individual lines in the table { #individual-lines }
If you want to add a single horizontal or vertical line in your table, for
example to separate a group of rows, you can use the [`table.hline`] and
[`table.vline`] elements for horizontal and vertical lines, respectively. Add
them to the argument list of the `table` function just like you would add
individual cells and a header.

Let's take a look at the following example from the reference:

```example
#set table.hline(stroke: 0.6pt)

#table(
  stroke: none,
  columns: (auto, 1fr),
  // Morning schedule abridged.
  [14:00], [Talk: Tracked Layout],
  [15:00], [Talk: Automations],
  [16:00], [Workshop: Tables],
  table.hline(),
  [19:00], [Day 1 Attendee Mixer],
)
```

In this example, you can see that we have placed a call to `table.hline` between
the cells, producing a horizontal line at that spot. We also used a set rule on
the element to reduce its stroke width to make it fit better with the weight of
the font.

By default, Typst places horizontal and vertical lines after the current row or
column, depending on their position in the argument list. You can also manually
move them to a different position by adding the `y` (for `hline`) or `x` (for
`vline`) argument. For example, the code below would produce the same result:

```typ
#set table.hline(stroke: 0.6pt)

#table(
  stroke: none,
  columns: (auto, 1fr),
  // Morning schedule abridged.
  table.hline(y: 3),
  [14:00], [Talk: Tracked Layout],
  [15:00], [Talk: Automations],
  [16:00], [Workshop: Tables],
  [19:00], [Day 1 Attendee Mixer],
)
```

Let's imagine you are working with a template that shows none of the table
strokes except for one between the first and second row. Now, since you have one
table that also has labels in the first column, you want to add an extra
vertical line to it. However, you do not want this vertical line to cross into
the top row. You can achieve this with the `start` argument:

```example
>>> #set page(width: 12cm)
>>> #show table.cell.where(y: 0): strong
>>> #set table(stroke: (_, y) => if y == 0 { (bottom: 1pt) })
// Base template already configured tables, but we need some
// extra configuration for this table.
#{
  set table(align: (x, _) => if x == 0 { left } else { right })
  show table.cell.where(x: 0): smallcaps
  table(
    columns: (auto, 1fr, 1fr, 1fr),
    table.vline(x: 1, start: 1),
    table.header[Trainset][Top Speed][Length][Weight],
    [TGV Réseau], [320 km/h], [200m], [383t],
    [ICE 403], [330 km/h], [201m], [409t],
    [Shinkansen N700], [300 km/h], [405m], [700t],
  )
}
```

In this example, we have added `table.vline` at the start of our positional
argument list. But because the line is not supposed to go to the left of the
first column, we specified the `x` argument as `{1}`. We also set the `start`
argument to `{1}` so that the line does only start after the first row.

The example also contains two more things: We use the align argument with a
function to right-align the data in all but the first column and use a show rule
to make the first column of table cells appear in small capitals. Because these
styles are specific to this one table, we put everything into a [code
block]($scripting/#blocks), so that the styling does not affect any further
tables.

### Overriding the strokes of a single cell { #stroke-override }
Imagine you want to change the stroke around a single cell. Maybe your cell is
very important and needs highlighting! For this scenario, there is the
[`table.cell` function]($table.cell). Instead of adding your content directly in
the argument list of the table, you wrap it in a `table.cell` call. Now, you can
use `table.cell`'s argument list to override the table properties, such as the
stroke, for this cell only.

Here's an example with a matrix of two of the Big Five personality factors, with
one intersection highlighted.

```example
>>> #set page(width: 16cm)
#table(
  columns: 3,
  stroke: (x: none),

  [], [*High Neuroticism*], [*Low Neuroticism*],

  [*High Agreeableness*],
  table.cell(stroke: orange + 2pt)[
    _Sensitive_ \ Prone to emotional distress but very empathetic.
  ],
  [_Compassionate_ \ Caring and stable, often seen as a supportive figure.],

  [*Low Agreeableness*],
  [_Contentious_ \ Competitive and easily agitated.],
  [_Detached_ \ Independent and calm, may appear aloof.],
)
```

Above, you can see that we used the `table.cell` element in the table's argument
list and passed the cell content to it. We have used its `stroke` argument to
set a wider orange stroke. Despite the fact that we disabled vertical strokes on
the table, the orange stroke appeared on all sides of the modified cell, showing
that the table's stroke configuration is overwritten.

### Complex document-wide stroke customization { #stroke-functions }
This section explains how to customize all lines at once in one or multiple
tables. This allows you to draw only the first horizontal line or omit the outer
lines, without knowing how many cells the table has. This is achieved by
providing a function to the table's `stroke` parameter. The function should
return a stroke given the zero-indexed x and y position of the current cell. You
should only need these functions if you are a template author, do not use a
template, or need to heavily customize your tables. Otherwise, your template
should set appropriate default table strokes.

For example, this is a set rule that draws all horizontal lines except for the
very first and last line.

```example
#show table.cell.where(x: 0): set text(style: "italic")
#show table.cell.where(y: 0): set text(style: "normal", weight: "bold")
#set table(stroke: (_, y) => if y > 0 { (top: 0.8pt) })

#table(
  columns: 3,
  align: center + horizon,
  table.header[Technique][Advantage][Drawback],
  [Diegetic], [Immersive], [May be contrived],
  [Extradiegetic], [Breaks immersion], [Obtrusive],
  [Omitted], [Fosters engagement], [May fracture audience],
)
```

In the set rule, we pass a function that receives two arguments, assigning the
vertical coordinate to `y` and discarding the horizontal coordinate. It then
returns a stroke dictionary with a `{0.8pt}` top stroke for all but the first
line. The cells in the first line instead implicitly receive `{none}` as the
return value. You can easily modify this function to just draw the inner
vertical lines instead as `{(x, _) => if x > 0 { (left: 0.8pt) }}`.

Let's try a few more stroking functions. The next function will only draw a line
below the first row:

```example
>>> #show table.cell: it => if it.x == 0 and it.y > 0 {
>>>   set text(style: "italic")
>>>   it
>>> } else {
>>>   it
>>> }
>>>
>>> #show table.cell.where(y: 0): strong
#set table(stroke: (_, y) => if y == 0 { (bottom: 1pt) })

<<< // Table as seen above
>>> #table(
>>>   columns: 3,
>>>   align: center + horizon,
>>>   table.header[Technique][Advantage][Drawback],
>>>   [Diegetic], [Immersive], [May be contrived],
>>>   [Extradiegetic], [Breaks immersion], [Obtrusive],
>>>   [Omitted], [Fosters engagement], [May fracture audience],
>>> )
```

If you understood the first example, it becomes obvious what happens here. We
check if we are in the first row. If so, we return a bottom stroke. Otherwise,
we'll return `{none}` implicitly.

The next example shows how to draw all but the outer lines:

```example
>>> #show table.cell: it => if it.x == 0 and it.y > 0 {
>>>   set text(style: "italic")
>>>   it
>>> } else {
>>>   it
>>> }
>>>
>>> #show table.cell.where(y: 0): strong
#set table(stroke: (x, y) => (
  left: if x > 0 { 0.8pt },
  top: if y > 0 { 0.8pt },
))

<<< // Table as seen above
>>> #table(
>>>   columns: 3,
>>>   align: center + horizon,
>>>   table.header[Technique][Advantage][Drawback],
>>>   [Diegetic], [Immersive], [May be contrived],
>>>   [Extradiegetic], [Breaks immersion], [Obtrusive],
>>>   [Omitted], [Fosters engagement], [May fracture audience],
>>> )
```

This example uses both the `x` and `y` coordinates. It omits the left stroke in
the first column and the top stroke in the first row. The right and bottom lines
are not drawn.

Finally, here is a table that draws all lines except for the vertical lines in
the first row and horizontal lines in the table body. It looks a bit like a
calendar.

```example
>>> #show table.cell: it => if it.x == 0 and it.y > 0 {
>>>   set text(style: "italic")
>>>   it
>>> } else {
>>>   it
>>> }
>>>
>>> #show table.cell.where(y: 0): strong
#set table(stroke: (x, y) => (
  left: if x == 0 or y > 0 { 1pt } else { 0pt },
  right: 1pt,
  top: if y <= 1 { 1pt } else { 0pt },
  bottom: 1pt,
))

<<< // Table as seen above
>>> #table(
>>>   columns: 3,
>>>   align: center + horizon,
>>>   table.header[Technique][Advantage][Drawback],
>>>   [Diegetic], [Immersive], [May be contrived],
>>>   [Extradiegetic], [Breaks immersion], [Obtrusive],
>>>   [Omitted], [Fosters engagement], [May fracture audience],
>>> )
```

This example is a bit more complex. We start by drawing all the strokes on the
right of the cells. But this means that we have drawn strokes in the top row,
too, and we don't need those! We use the fact that `left` will override `right`
and only draw the left line if we are not in the first row or if we are in the
first column. In all other cases, we explicitly remove the left line. Finally,
we draw the horizontal lines by first setting the bottom line and then for the
first two rows with the `top` key, suppressing all other top lines. The last
line appears because there is no `top` line that could suppress it.

### How to achieve a double line? { #double-stroke }
Typst does not yet have a native way to draw double strokes, but there are
multiple ways to emulate them, for example with [tilings]($tiling). We will
show a different workaround in this section: Table gutters.

Tables can space their cells apart using the `gutter` argument. When a gutter is
applied, a stroke is drawn on each of the now separated cells. We can
selectively add gutter between the rows or columns for which we want to draw a
double line. The `row-gutter` and `column-gutter` arguments allow us to do this.
They accept arrays of gutter values. Let's take a look at an example:

```example
#table(
  columns: 3,
  stroke: (x: none),
  row-gutter: (2.2pt, auto),
  table.header[Date][Exercise Type][Calories Burned],
  [2023-03-15], [Swimming], [400],
  [2023-03-17], [Weightlifting], [250],
  [2023-03-18], [Yoga], [200],
)
```

We can see that we used an array for `row-gutter` that specifies a `{2.2pt}` gap
between the first and second row. It then continues with `auto` (which is the
default, in this case `{0pt}` gutter) which will be the gutter between all other
rows, since it is the last entry in the array.

## How to align the contents of the cells in my table? { #alignment }
You can use multiple mechanisms to align the content in your table. You can
either use the `table` function's `align` argument to set the alignment for your
whole table (or use it in a set rule to set the alignment for tables throughout
your document) or the [`align`] function (or `table.cell`'s `align` argument) to
override the alignment of a single cell.

When using the `table` function's align argument, you can choose between three
methods to specify an [alignment]:

- Just specify a single alignment like `right` (aligns in the top-right corner)
  or `center + horizon` (centers all cell content). This changes the alignment
  of all cells.
- Provide an array. Typst will cycle through this array for each column.
- Provide a function that is passed the horizontal `x` and vertical `y`
  coordinate of a cell and returns an alignment.

For example, this travel itinerary right-aligns the day column and left-aligns
everything else by providing an array in the `align` argument:

```example
>>> #set page(width: 12cm)
#set text(font: "IBM Plex Sans")
#show table.cell.where(y: 0): set text(weight: "bold")

#table(
  columns: 4,
  align: (right, left, left, left),
  fill: (_, y) => if calc.odd(y) { green.lighten(90%) },
  stroke: none,

  table.header[Day][Location][Hotel or Apartment][Activities],
  [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise],
  [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower],
  [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting],
  [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum],
  [5], [Zermatt, Switzerland], [Alpine Lodge], [Visit Matterhorn, Skiing],
)
```

However, this example does not yet look perfect — the header cells should be
bottom-aligned. Let's use a function instead to do so:

```example
>>> #set page(width: 12cm)
#set text(font: "IBM Plex Sans")
#show table.cell.where(y: 0): set text(weight: "bold")

#table(
  columns: 4,
  align: (x, y) =>
    if x == 0 { right } else { left } +
    if y == 0 { bottom } else { top },
  fill: (_, y) => if calc.odd(y) { green.lighten(90%) },
  stroke: none,

  table.header[Day][Location][Hotel or Apartment][Activities],
  [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise],
  [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower],
<<<  // ... remaining days omitted
>>>  [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting],
>>>  [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum],
>>>  [5], [Zermatt, Switzerland], [Alpine Lodge], [Visit Matterhorn, Skiing],
)
```

In the function, we calculate a horizontal and vertical alignment based on
whether we are in the first column (`{x == 0}`) or the first row (`{y == 0}`).
We then make use of the fact that we can add horizontal and vertical alignments
with `+` to receive a single, two-dimensional alignment.

You can find an example of using `table.cell` to change a single cell's
alignment on [its reference page]($table.cell).

## How to merge cells? { #merge-cells }
When a table contains logical groupings or the same data in multiple adjacent
cells, merging multiple cells into a single, larger cell can be advantageous.
Another use case for cell groups are table headers with multiple rows: That way,
you can group for example a sales data table by quarter in the first row and by
months in the second row.

A merged cell spans multiple rows and/or columns. You can achieve it with the
[`table.cell`] function's `rowspan` and `colspan` arguments: Just specify how
many rows or columns you want your cell to span.

The example below contains an attendance calendar for an office with in-person
and remote days for each team member. To make the table more glanceable, we
merge adjacent cells with the same value:

```example
>>> #set page(width: 22cm)
#let ofi = [Office]
#let rem = [_Remote_]
#let lea = [*On leave*]

#show table.cell.where(y: 0): set text(
  fill: white,
  weight: "bold",
)

#table(
  columns: 6 * (1fr,),
  align: (x, y) => if x == 0 or y == 0 { left } else { center },
  stroke: (x, y) => (
    // Separate black cells with white strokes.
    left: if y == 0 and x > 0 { white } else { black },
    rest: black,
  ),
  fill: (_, y) => if y == 0 { black },

  table.header(
    [Team member],
    [Monday],
    [Tuesday],
    [Wednesday],
    [Thursday],
    [Friday]
  ),
  [Evelyn Archer],
    table.cell(colspan: 2, ofi),
    table.cell(colspan: 2, rem),
    ofi,
  [Lila Montgomery],
    table.cell(colspan: 5, lea),
  [Nolan Pearce],
    rem,
    table.cell(colspan: 2, ofi),
    rem,
    ofi,
)
```

In the example, we first define variables with "Office", "Remote", and "On
leave" so we don't have to write these labels out every time. We can then use
these variables in the table body either directly or in a `table.cell` call if
the team member spends multiple consecutive days in office, remote, or on leave.

The example also contains a black header (created with `table`'s `fill`
argument) with white strokes (`table`'s `stroke` argument) and white text (set
by the `table.cell` set rule). Finally, we align all the content of all table
cells in the body in the center. If you want to know more about the functions
passed to `align`, `stroke`, and `fill`, you can check out the sections on
[alignment], [strokes](#stroke-functions), and [striped
tables](#fills).

This table would be a great candidate for fully automated generation from an
external data source! Check out the [section about importing
data](#importing-data) to learn more about that.

## How to rotate a table? { #rotate-table }
When tables have many columns, a portrait paper orientation can quickly get
cramped. Hence, you'll sometimes want to switch your tables to landscape
orientation. There are two ways to accomplish this in Typst:

- If you want to rotate only the table but not the other content of the page and
  the page itself, use the [`rotate` function]($rotate) with the `reflow`
  argument set to `{true}`.
- If you want to rotate the whole page the table is on, you can use the [`page`
  function]($page) with its `flipped` argument set to `{true}`. The header,
  footer, and page number will now also appear on the long edge of the page.
  This has the advantage that the table will appear right side up when read on a
  computer, but it also means that a page in your document has different
  dimensions than all the others, which can be jarring to your readers.

Below, we will demonstrate both techniques with a student grade book table.

First, we will rotate the table on the page. The example also places some text
on the right of the table.

```example
#set page("a5", columns: 2, numbering: "— 1 —")
>>> #set page(margin: auto)
#show table.cell.where(y: 0): set text(weight: "bold")

#rotate(
  -90deg,
  reflow: true,

  table(
    columns: (1fr,) + 5 * (auto,),
    inset: (x: 0.6em,),
    stroke: (_, y) => (
      x: 1pt,
      top: if y <= 1 { 1pt } else { 0pt },
      bottom: 1pt,
    ),
    align: (left, right, right, right, right, left),

    table.header(
      [Student Name],
      [Assignment 1], [Assignment 2],
      [Mid-term], [Final Exam],
      [Total Grade],
    ),
    [Jane Smith], [78%], [82%], [75%], [80%], [B],
    [Alex Johnson], [90%], [95%], [94%], [96%], [A+],
    [John Doe], [85%], [90%], [88%], [92%], [A],
    [Maria Garcia], [88%], [84%], [89%], [85%], [B+],
    [Zhang Wei], [93%], [89%], [90%], [91%], [A-],
    [Marina Musterfrau], [96%], [91%], [74%], [69%], [B-],
  ),
)

#lorem(80)
```


What we have here is a two-column document on ISO A5 paper with page numbers on
the bottom. The table has six columns and contains a few customizations to
[stroke](#strokes), alignment and spacing. But the most important part is that
the table is wrapped in a call to the `rotate` function with the `reflow`
argument being `{true}`. This will make the table rotate 90 degrees
counterclockwise. The reflow argument is needed so that the table's rotation
affects the layout. If it was omitted, Typst would lay out the page as if the
table was not rotated (`{true}` might become the default in the future).

The example also shows how to produce many columns of the same size: To the
initial `{1fr}` column, we add an array with five `{auto}` items that we
create by multiplying an array with one `{auto}` item by five. Note that arrays
with just one item need a trailing comma to distinguish them from merely
parenthesized expressions.

The second example shows how to rotate the whole page, so that the table stays
upright:

```example
#set page("a5", numbering: "— 1 —")
>>> #set page(margin: auto)
#show table.cell.where(y: 0): set text(weight: "bold")

#page(flipped: true)[
  #table(
    columns: (1fr,) + 5 * (auto,),
    inset: (x: 0.6em,),
    stroke: (_, y) => (
      x: 1pt,
      top: if y <= 1 { 1pt } else { 0pt },
      bottom: 1pt,
    ),
    align: (left, right, right, right, right, left),

    table.header(
      [Student Name],
      [Assignment 1], [Assignment 2],
      [Mid-term], [Final Exam],
      [Total Grade],
    ),
    [Jane Smith], [78%], [82%], [75%], [80%], [B],
    [Alex Johnson], [90%], [95%], [94%], [96%], [A+],
    [John Doe], [85%], [90%], [88%], [92%], [A],
    [Maria Garcia], [88%], [84%], [89%], [85%], [B+],
    [Zhang Wei], [93%], [89%], [90%], [91%], [A-],
    [Marina Musterfrau], [96%], [91%], [74%], [69%], [B-],
  )

  #pad(x: 15%, top: 1.5em)[
    = Winter 2023/24 results
    #lorem(80)
  ]
]
```

Here, we take the same table and the other content we want to set with it and
put it into a call to the [`page`] function while supplying `{true}` to the
`flipped` argument. This will instruct Typst to create new pages with width and
height swapped and place the contents of the function call onto a new page.
Notice how the page number is also on the long edge of the paper now. At the
bottom of the page, we use the [`pad`] function to constrain the width of the
paragraph to achieve a nice and legible line length.

## How to break a table across pages? { #table-across-pages }
It is best to contain a table on a single page. However, some tables just have
many rows, so breaking them across pages becomes unavoidable. Fortunately, Typst
supports breaking tables across pages out of the box. If you are using the
[`table.header`] and [`table.footer`] functions, their contents will be repeated
on each page as the first and last rows, respectively. If you want to disable
this behavior, you can set `repeat` to `{false}` on either of them.

If you have placed your table inside of a [figure], it becomes unable to break
across pages by default. However, you can change this behavior. Let's take a
look:

```example
#set page(width: 9cm, height: 6cm)
#show table.cell.where(y: 0): set text(weight: "bold")
#show figure: set block(breakable: true)

#figure(
  caption: [Training regimen for Marathon],
  table(
    columns: 3,
    fill: (_, y) => if y == 0 { gray.lighten(75%) },

    table.header[Week][Distance (km)][Time (hh:mm:ss)],
    [1], [5],  [00:30:00],
    [2], [7],  [00:45:00],
    [3], [10], [01:00:00],
    [4], [12], [01:10:00],
    [5], [15], [01:25:00],
    [6], [18], [01:40:00],
    [7], [20], [01:50:00],
    [8], [22], [02:00:00],
    [...], [...], [...],
    table.footer[_Goal_][_42.195_][_02:45:00_],
  )
)
```

A figure automatically produces a [block] which cannot break by default.
However, we can reconfigure the block of the figure using a show rule to make it
`breakable`. Now, the figure spans multiple pages with the headers and footers
repeating.

## How to import data into a table? { #importing-data }
Often, you need to put data that you obtained elsewhere into a table. Sometimes,
this is from Microsoft Excel or Google Sheets, sometimes it is from a dataset
on the web or from your experiment. Fortunately, Typst can load many [common
file formats]($category/data-loading), so you can use scripting to include their
data in a table.

The most common file format for tabular data is CSV. You can obtain a CSV file
from Excel by choosing "Save as" in the _File_ menu and choosing the file format
"CSV UTF-8 (Comma-delimited) (.csv)". Save the file and, if you are using the
web app, upload it to your project.

In our case, we will be building a table about Moore's Law. For this purpose, we
are using a statistic with [how many transistors the average microprocessor
consists of per year from Our World in
Data](https://ourworldindata.org/grapher/transistors-per-microprocessor). Let's
start by pressing the "Download" button to get a CSV file with the raw data.

Be sure to move the file to your project or somewhere Typst can see it, if you
are using the CLI. Once you did that, we can open the file to see how it is
structured:

```csv
Entity,Code,Year,Transistors per microprocessor
World,OWID_WRL,1971,2308.2417
World,OWID_WRL,1972,3554.5222
World,OWID_WRL,1974,6097.5625
```

The file starts with a header and contains four columns: Entity (which is to
whom the metric applies), Code, the year, and the number of transistors per
microprocessor. Only the last two columns change between each row, so we can
disregard "Entity" and "Code".

First, let's start by loading this file with the [`csv`] function. It accepts
the file name of the file we want to load as a string argument:

```typ
#let moore = csv("moore.csv")
```

We have loaded our file (assuming we named it `moore.csv`) and [bound
it]($scripting/#bindings) to the new variable `moore`. This will not produce any
output, so there's nothing to see yet. If we want to examine what Typst loaded,
we can either hover the name of the variable in the web app or print some items
from the array:

```example
#let moore = csv("moore.csv")

#moore.slice(0, 3)
```

With the arguments `{(0, 3)}`, the [`slice`]($array.slice) method returns the
first three items in the array (with the indices 0, 1, and 2). We can see that
each row is its own array with one item per cell.

Now, let's write a loop that will transform this data into an array of cells
that we can use with the table function.

```example
#let moore = csv("moore.csv")

#table(
  columns: 2,
  ..for (.., year, count) in moore {
    (year, count)
  }
)
```

The example above uses a for loop that iterates over the rows in our CSV file
and returns an array for each iteration. We use the for loop's
[destructuring]($scripting/#bindings) capability to discard all but the last two
items of each row. We then create a new array with just these two. Because Typst
will concatenate the array results of all the loop iterations, we get a
one-dimensional array in which the year column and the number of transistors
alternate. We can then insert the array as cells. For this we use the [spread
operator]($arguments/#spreading) (`..`). By prefixing an array, or, in our case
an expression that yields an array, with two dots, we tell Typst that the
array's items should be used as positional arguments.

Alternatively, we can also use the [`map`]($array.map), [`slice`]($array.slice),
and [`flatten`]($array.flatten) array methods to write this in a more functional
style:

```typ
#let moore = csv("moore.csv")

#table(
   columns: moore.first().len(),
   ..moore.map(m => m.slice(2)).flatten(),
)
```

This example renders the same as the previous one, but first uses the `map`
function to change each row of the data. We pass a function to map that gets run
on each row of the CSV and returns a new value to replace that row with. We use
it to discard the first two columns with `slice`. Then, we spread the data into
the `table` function. However, we need to pass a one-dimensional array and
`moore`'s value is two-dimensional (that means that each of its row values
contains an array with the cell data). That's why we call `flatten` which
converts it to a one-dimensional array. We also extract the number of columns
from the data itself.

Now that we have nice code for our table, we should try to also make the table
itself nice! The transistor counts go from millions in 1995 to trillions in 2021
and changes are difficult to see with so many digits. We could try to present
our data logarithmically to make it more digestible:

```example
#let moore = csv("moore.csv")
#let moore-log = moore.slice(1).map(m => {
  let (.., year, count) = m
  let log = calc.log(float(count))
  let rounded = str(calc.round(log, digits: 2))
  (year, rounded)
})

#show table.cell.where(x: 0): strong

#table(
   columns: moore-log.first().len(),
   align: right,
   fill: (_, y) => if calc.odd(y) { rgb("D7D9E0") },
   stroke: none,

   table.header[Year][Transistor count ($log_10$)],
   table.hline(stroke: rgb("4D4C5B")),
   ..moore-log.flatten(),
)
```

In this example, we first drop the header row from the data since we are adding
our own. Then, we discard all but the last two columns as above. We do this by
[destructuring]($scripting/#bindings) the array `m`, discarding all but the two
last items. We then convert the string in `count` to a floating point number,
calculate its logarithm and store it in the variable `log`. Finally, we round it
to two digits, convert it to a string, and store it in the variable `rounded`.
Then, we return an array with `year` and `rounded` that replaces the original
row. In our table, we have added our custom header that tells the reader that
we've applied a logarithm to the values. Then, we spread the flattened data as
above.

We also styled the table with [stripes](#fills), a
[horizontal line](#individual-lines) below the first row, [aligned](#alignment)
everything to the right, and emboldened the first column. Click on the links to
go to the relevant guide sections and see how it's done!

## What if I need the table function for something that isn't a table? { #table-and-grid }
Tabular layouts of content can be useful not only for matrices of closely
related data, like shown in the examples throughout this guide, but also for
presentational purposes. Typst differentiates between grids that are for layout
and presentational purposes only and tables, in which the arrangement of the
cells itself conveys information.

To make this difference clear to other software and allow templates to heavily
style tables, Typst has two functions for grid and table layout:

- The [`table`] function explained throughout this guide which is intended for
  tabular data.
- The [`grid`] function which is intended for presentational purposes and page
  layout.

Both elements work the same way and have the same arguments. You can apply
everything you have learned about tables in this guide to grids. There are only
three differences:

- You'll need to use the [`grid.cell`], [`grid.vline`], and [`grid.hline`]
  elements instead of [`table.cell`], [`table.vline`], and [`table.hline`].
- The grid has different defaults: It draws no strokes by default and has no
  spacing (`inset`) inside of its cells.
- Elements like `figure` do not react to grids since they are supposed to have
  no semantical bearing on the document structure.