I wrote my PhD thesis in Typst — here are some screenshots and snippets!

I wrote my PhD thesis on AI and Natural Language Processing entirely in Typst! The overall experience was great, especially compared to my previous thesis, which I wrote in LaTeX (and which involved many hours of frustration…).

I didn’t use any official template, as my university didn’t provide one. Instead, I built the entire document from scratch, which turned out to be much easier than I expected. I was able to achieve almost exactly the layout and style I had in mind, with the possible exception of the bibliography, which seems to be a common challenge for thesis writing in Typst.

I could probably have taken advantage of more packages, but in the end I mainly relied on cetz and fletcher for diagrams, hydra for chapter headers, and flagada for national flags (I included several examples featuring text translated into different languages). I would also have loved to recreate my result charts using lilaq , but time was running short, so I ended up embedding them as PDFs.

I hope this might be useful or encouraging to someone considering Typst for a large academic project. Below are a couple of screenshots, along with some code snippets!


The code for the Transformer architecture figure from Vaswani et al. (2017):

#import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge
#import fletcher.shapes: house, hexagon, trapezium
#import "@preview/cetz:0.5.2": canvas, draw.bezier

#set par(leading: 0.5em)

#let blob(
  pos, 
  label, 
  tint: color.white.transparentize(100%), 
  ..args
) = node(
  pos, 
  align(center, label),
  width: 3cm,
  fill: tint.lighten(60%),
  stroke: 1pt + tint.darken(20%),
  corner-radius: 5pt,
  inset: 5pt,
  ..args,
)

// Renders a "block" of blobs with the main element in the center, the "Add & Norm" blob on top, and the dashed arrow on the side.
#let blob-add-norm(
  pos, 
  group-name, 
  body, 
  tint: orange, 
  incoming-arrow-points: (-1, +1),
  ..args
) = {
  if type(group-name) == label {
    group-name = str(group-name)
  }
  // Main blob
  let main-label = label(group-name + "-main")
  blob(pos, name: main-label, [#body], tint: tint, ..args)
  edge()
  
  // Add & Norm blob
  let add-label = label(group-name + "-add")
  blob((rel: (0cm, 0.5cm), to: label(group-name + "-main.north")), name: add-label, [Add & Norm], tint: yellow, shape: hexagon, width: 2.8cm)
  
  // Discriminate encoder (left) and decoder (right) blobs
  let side = if group-name.starts-with("enc") {-2.2cm} else {2.2cm}
  // Edge from init to Add & Norm
  let btm-distance = if incoming-arrow-points.len() > 0 {0.8cm} else {0.4cm}
  let bottom-origin-point = (rel: (0cm, -btm-distance), to: label(group-name + "-main.south"))
  edge(
    bottom-origin-point,
    (rel: (side, 0cm), to: bottom-origin-point),
    (rel: (side, 0cm), to: add-label),
    add-label,
    "--|>",
  )
  for x in incoming-arrow-points {
    edge(
      (rel: (0cm, -0.4cm), to: label(group-name + "-main.south")), 
      (rel: (x * 1cm, -0.4cm), to: label(group-name + "-main.south")),
      (rel: (x * 1cm, 0cm), to: label(group-name + "-main.south")),
      "-|>",
    )
  }
  node(
    enclose: (main-label, add-label, (rel: (side, 0cm), to: bottom-origin-point)),
    stroke: none,
    inset: 1pt,
    name: label(group-name),
  )
}

#diagram(
  edge-stroke: 1pt,
  edge-corner-radius: 5pt,
  mark-scale: 70%,
  {
    blob((0cm, -0.5cm), name: <input>, [Inputs], stroke: none)
    edge()
    blob((0cm, 1cm), name: <input-embs>, [Input\ Embeddings], width: auto, tint: red.lighten(50%), shape: trapezium)

    edge(<enc-mhatt-main>, "-|>")
    blob-add-norm((0cm, 5cm), <enc-mhatt>, tint: orange)[Multi-Head\ Attention]
    
    edge(<enc-mhatt-add>, <enc-ff-main>, "-|>")
    blob-add-norm((0cm, 8cm), <enc-ff>, tint: aqua, incoming-arrow-points: ())[Feed\ Forward]
  
    node(
      name: <enc>,
      enclose: (
        <enc-mhatt>, 
        <enc-ff>
      ), 
      inset: 3mm, 
      fill: gray.transparentize(80%),
      stroke: 1pt + gray,
      corner-radius: 14pt,
    )
  
    let s = 5cm
    
    blob((s, -0.5cm), [Outputs\ (shifted right)], name: <output>, stroke: none)
    edge()
    blob((s, 1cm), name: <output-embs>, [Output\ Embeddings], width: auto, tint: red.lighten(50%), shape: trapezium)
  
    edge(<dec-mask-main>, "-|>")
    blob-add-norm((s, 5.2cm), <dec-mask>, tint: orange)[Masked\ Multi-Head\ Attention]
    
    edge(<dec-mask-add>, <dec-mhatt-main>, "-|>")
    blob-add-norm((s, 8.7cm), <dec-mhatt>, tint: orange, incoming-arrow-points: (1, ))[Multi-Head\ Attention]
    
    edge(<dec-mhatt-add>, <dec-ff-main>, "-|>")
    blob-add-norm((s, 11.5cm), <dec-ff>, tint: aqua, incoming-arrow-points: ())[Feed\ Forward]
  
    node(
      name: <dec>,
      enclose: (
        <dec-mask>,
        <dec-mhatt>, 
        <dec-ff>
      ), 
      inset: 3mm, 
      fill: gray.transparentize(80%),
      stroke: 1pt + gray,
      corner-radius: 14pt,
    )

    edge(
      <enc-ff-add.north>, 
      (rel: (0cm, 1cm)), 
      (rel: (s / 2, 0cm)), 
      (rel: (-s / 2, -0.5cm), to: <dec-mhatt-main.south>), 
      (rel: (-0.4cm, -0.5cm), to: <dec-mhatt-main.south>), 
      (rel: (-0.4cm, 0cm), to: <dec-mhatt-main.south>), 
      "-|>",
    )

    edge(<dec-mhatt-add>, <linear>, "-|>")
    blob((s, 14cm), name: <linear>, [Linear], tint: purple.lighten(50%))
    edge("-|>")
    blob((s, 15.2cm), name: <softmax>, [Softmax], tint: green)
    edge("-|>")
    blob((s, 16.8cm), [Output\ Probabilities],)
    
    node((rel: (-0.5cm, 0cm), to: <enc.west>))[N$times$]
    node((rel: (0.5cm, 0cm), to: <dec.east>))[N$times$]

    let pos-enc = circle(radius: 12pt)[#canvas(bezier((-12pt, 0), (12pt, 0), (0, 16pt), (0, -16pt)))]
    
    node((0cm, 2.5cm), inset: 0mm, label: <enc-plus>)[#text(size: 1.5em)[$plus.o$]]
    edge()
    node((-2cm, 2.5cm), inset: 0mm)[#stack(dir: ltr, spacing: 8pt, align(right)[Positional\ Encoding], pos-enc)]
    
    node((s, 2.5cm), inset: 0mm, label: <dec-plus>)[#text(size: 1.5em)[$plus.o$]]
    edge()
    node((s + 2cm, 2.5cm), inset: 0mm)[#stack(dir: ltr, spacing: 8pt, pos-enc, align(left)[Positional\ Encoding])]
  }
)
3 Likes

Nice! I changed one of the tags (five is the maximum); if you disagree, feel free to change it back (or message me if you can’t), but large-document felt highly relevant here.

Is this thesis already defended, or is that in the future? Not too significant in the grand scheme of things, but once you got your PhD through this thesis, I’d love to mark the relevant square in the (Unofficial) Typst 2026 Bingo! :wink: