I’m not sure if a table should ever look like that. 
But if you really like (or need) it, here you go:
#let header-cell-frame(angle, height, width, stroke) = polygon(
stroke: stroke,
(height * calc.tan(angle - 90deg), 0pt),
(0pt, 100%),
(width, 100%),
(height * calc.tan(angle - 90deg) + width, 0pt),
)
#let header-cell-body(angle, body) = rotate(
angle,
reflow: true,
place(horizon + left, dx: 1em, body)
)
#let header-cell(angle, height, width, stroke, body) = table.cell(
inset: 0pt,
block(
height: height,
width: width,
{
place(bottom + left, header-cell-frame(angle, height, width, stroke))
place(bottom + left, dx: width / 2, header-cell-body(angle, body))
}
)
)
#let show-rotated-header(angle, height, columns: auto) = it => context {
if "label" in it.fields() and it.label == <rotated-header> { return it }
let (header, ..children) = it.children
let column-widths = if columns == auto {
header.children.map(cell => measure(cell).width + 2 * it.inset)
} else {
columns
}
[
#table(
columns: column-widths,
stroke: (x, y) => if y == 0 { none } else { it.stroke },
table.header(
..column-widths.zip(header.children).map(
((width, child)) => header-cell(angle, height, width, it.stroke, child.body)
)
),
..children
) <rotated-header>
]
}
#show table: show-rotated-header(-45deg, 2cm)
#table(
columns: 4,
table.header([*Students*], "Maths", "Physics", "French"),
[Name 1],[],[],[],
[Name 2],[],[],[],
[Name 3],[],[],[],
)
The table will look like this
How does this work?
The rule show-rotated-header()
will recreate the table with a new header. The label <rotated-header>
is used to ensure that the show rule is only applied once.
By default the new table will use the column widths of the initial table without the rotated header. You can however also specify the column widths manually. The stroke in the header row is hidden since these cells will get a custom frame.
Each header cell is composed of a polygon to draw the angled frame, and the initial body of the cell rotated by the same angle. The polygons use the same stroke as the table.
Possible changes/improvements
- Find the maximum width in each column for the automatic
column-widths
. Right now the widths of the cells in the initial header are used.
- Set the height to the maximum height of the rotated header cells.
- Transfer all fields from the initial table to the new table. Right now only
columns
and stroke
are inherited.
- Implement this as a function instead of a show rule if you don’t want to style every table in your document like this. If you need to customize the column widths for each table separately, this will definitely be the cleaner solution.