Typst template

2026-01-31

In a previous post I created a LaTeX template to keep track of my preferred configuration for typesetting notes and reports. I still use LaTeX a lot, but I’ve been getting interested in Typst , a modern document typesetting system that could be a viable replacement. Typst fixes a lot of issues that made LaTeX annoying to use:

In order to learn how Typst works, I tried to re-create my LaTeX document template. I also experimented with a few new features.

These are the basic verbs of Typst:

I will go through the template line by line:

Firstly import external packages. * means import all functions from these packages. Compared to LaTeX, I need far fewer packages to create a nice looking document.

#import "@preview/booktabs:0.0.4": *  // booktabs style tables
#import "@preview/zero:0.6.0": *  // similar to siunitx

Define options for the template, which can be set in the document which calls the template.

// mynotes.typ
#let project(
    title: "", 
    authors: (), 
    line_numbers: false, 
    draft_mode: false, 
    anonymous: false,
    spacing: "normal",
    
    body) = {

Basic document settings, including the title, authors, page geometry and fonts. 2.54 cm margins matches the default for Microsoft Word documents. Pages are numbered, with numbers at the bottom of the page in the centre. The font is “CMU Serif”, which was designed by Donald Knuth for TeX.

  // Set document metadata
  set document(title: title, author: authors)

  // Page geometry
  set page(
    paper: "a4",
    margin: (
      top: 2.54cm, 
      bottom: 2.54cm,
      left: 2.54cm,
      right: 2.54cm
    ),
    numbering: "1", 
    number-align: center, 
  )

  // Font
  set text(
    font: "CMU Serif", 
    size: 11pt,
    lang: "en",
    region: "gb"
  )

Adjust paragraph and heading spacing. Colour hyperlinks. Bump the bibliography to a new page.

  // Heading numbering
  set heading(
    numbering: "1."
  )

  // Paragraph spacing 
  set par(
    spacing: 0.55cm
  )

  // Heading spacing
  show heading: set block(
      above: 1.5em, 
      below: 1.0em
    )

  // Heading text size
  show heading: set text(size: 1.1em)
  
  // Hyperlink colour
  show link: set text(fill: rgb("#336666"))

  // Bibliography on a new page
  show bibliography: it => {
    pagebreak()
    it
  }

Define a toggle for line numbering. If the option “line_numbers: true” when the template is called, the document will have line numbers.

  // Line number toggle
  show: it => {
    if line_numbers {
      set par.line(numbering: "1")
      it
    } else {
      it
    }
  }

This is one of the new features, a toggle which adds a “DRAFT” watermark across the page.

  // Draft mode toggle
  show: it => {
    if draft_mode {
      set page(background: rotate(45deg, text(80pt, fill: rgb("EEEEEE"))[DRAFT]))
      it
    } else {
      it
    }
  }

Another new feature to toggle the spacing of the document. Useful if I want to print and annotate a draft.

  // Spacing toggle
  show: it => {
    if spacing == "compact" {
      set par(leading: 0.5em, justify: true)
      set page(margin: 1.5cm)
      it
    } else if spacing == "wide" {
      set par(leading: 1.5em) 
      it
    } else {
      it
    }
  }

Another new feature to anonymise the authors by replacing the names with a black bar.

  // Control for potentially multiple authors
  let authors = if type(authors) == str { (authors,) } else { authors }

  // Anonymous author names toggle
  show: it => {
    if anonymous {
      for author in authors {
        show author: name => box(fill: black, radius: 1pt, hide(name))
      }
      it
    } else { it }
  }

  // Title and author formatting
  align(center)[
    #block(text(size: 2em, weight: "bold")[#title])

    #text(size: 1em)[
      #{
        let names = authors.join(", ", last: ", and ")
        if anonymous {
          // Optional redaction of author names
          box(fill: black, radius: 1pt, hide(names))
        } else {
          names
        }
      }
    ]
    #v(1em) 
  ]

Set the style for tables and figures.

  // Reset table style
  set table(
    stroke: none,
    gutter: 0pt,
    inset: 0.5em,
  )

  // Use booktabs default style
  show table: it => {
    booktabs-default-table-style(it)
  }

  // Move table captions above, but keep image captions below
  show figure.where(kind: table): set figure.caption(position: top)

  // Left-align figure captions
  show figure.caption: it => {
    block(width: 90%, align(left)[
      #it
    ])
  }

  // Add space between the caption and the table
  show figure.where(kind: table): set block(spacing: 1.5em)

  // Render content
  body
}

Define a new variable to highlight TODO items in red with a pale red background and prefixed with “TODO:”.

// Define todo function
#let todo(body) = {
  highlight(fill: red.lighten(80%))[
    #text(fill: red.darken(20%), weight: "bold")[TODO:] #body
  ]
}

Configure the zero package, which formats numbers.

// Configure {zero}
#set-group(
  size: 3, 
  separator: ",", 
  threshold: (integer: 4, fractional: calc.inf),
)

Then in the main document I can do this to call the template if the template is in the project directory:

#import "mynotes.typ": *

#show: project.with(
  title: "Testing the Typst template",
  authors: ("John L. Godlee"),
  line_numbers: false,
  draft_mode: false,
  anonymous: false,
  spacing: "normal",
)

I can also put the template here to call it from anywhere:

Library/Application Support/typst/
└── packages
    └── local
        └── mynotes
            └── 0.1.0
                ├── mynotes.typ
                └── typst.toml

The typst.toml looks like this:

[package]
name = "mynotes"
version = "0.1.0"
entrypoint = "mynotes.typ"

Then I can call it in a document like so:

#import "@local/mynotes:0.1.0": *

#show: project.with(
  title: "Testing the Typst template",
  authors: ("John L. Godlee"),
  line_numbers: false,
  draft_mode: false,
  anonymous: false,
  spacing: "normal",
)
Screenshot of Typst document.
Screenshot of Typst document.

To streamline the editing process I am using the chomosuke/typst-preview.nvim neovim plugin, which lets you compile Typst documents inside neovim, and provides a live preview of the document in a web browser. It relies on the tinymist LSP. This is the config I’m using with Lazy.nvim:

-- Typst
return { "chomosuke/typst-preview.nvim",
	ft = "typst", -- Only load when opening Typst files
	version = "1.*",

	build = function()
		require("typst-preview").update()
	end,

	config = function()
		require("typst-preview").setup({
			debug = false,
			render_on_save = false,
			follow_cursor = true,
			invert_colors = "auto",
		})
	end,
}