Anatomy of a template
Typst is a computer program for typesetting documents. Developed by two graduate students from Technical University of Berlin, it is thoughtfully designed to be fast, portable, feature-packed, and script-able. It wouldn’t be unfair to call it the spiritual successor to LaTeX. It is evolving well and taking-on LaTeX’s handicaps with contributions from talented open-source enthusiasts, e.g.,
- compilation speed
- learning curve
- mark-up verbosity
- multi-run demand for referencing
- package size
- syntax highlighting
With good defaults (paper size, font-face, font-size, margins, paragraph properties, etc.), one can get going without needing a single line of preamble code. But if you are a seasoned LaTeX user, then you’d know how useful document classes are — something Typst does not yet come pre-packaged with. A little universe is starting to form — for the users, by the users. If you’re one of them, like I am, and your tastes (or needs) are slightly different from those available there-in, then it would not hurt to know how to write a Typst template from scratch. Here is how — in this note-to-self, like much of this website.
Rule One of publishing is to separate presentation (styling) from content, be it offline or online. So, we create two files:
- main.typ — this file will have all the report’s content
- template.typ — this one’s for the report’s presentation + styling
main.typ: This requires only a few key user-inputs (which would be variables pre-defined in template.typ), e.g. document title, name of the author, and paper size (useful for me) other than the content itself.
#import "template.typ": *
#show: note.with(
author: "C Kunte",
paper: "a4",
title: [On-bottom stability],
)
// report content from here-on
template.typ: Basic preamble is as follows. Defaults will be used when things like margins are undefined in either of the two files.
#let note(
author: "Author",
paper: "a4",
title: [Note title],
body,
) = {
// metadata
set document(title: title, author: author)
// page properties
set page(
paper: paper, // enables user-definable in main.typ
numbering: "1",
)
// print title block (includes title, author, date)
align(center)[
#text(2em)[*#title*]
#v(2em, weak: true)
#text(1em, author)
#v(1em, weak: true)
#datetime.today().display("[month repr:long] [day], [year]")
#v(5em, weak: true)
]
// content from here-on
body
}
If I were to automate setting margins based on paper size, then it would be like so, in which top margin overrides y parameter to make room for a custom header.
set page(
// set margins based on paper size
margin: if paper == "a5" {
(x: 0.75in, y: 0.75in, top: 0.9in)
} else {
(x: 1.0in, y: 1.0in, top: 1.25in)
},
)
We can do something similar for font size:
set text(
size: if paper == "a5" { 11pt } else { 12pt },
)
If you’re a typographical nut, then you’re covered with features to tune your text too, e.g.,
set text(
font: "Segoe UI", // e.g. "STIX Two Text" or "erewhon",
top-edge: "cap-height",
bottom-edge: "baseline",
number-type: "old-style",
size: if paper == "a5" { 11pt } else { 12pt },
)
Like indented classic look for paragraphs? Then you can do this:
set par(
spacing: 0.65em,
leading: 0.65em,
first-line-indent: 12pt,
justify: true
)
Here is a little complex header code to make title of the note (paper) appear on every even page, and the active section name appear on every odd page other than the first.
set page(
header: context {
// set custom header: make title appear on even pages
if calc.even(counter(page).get().first()) {
emph(title)
} else { none }
// make section appear on odd pages other than the first
let page-num = counter(page).get().first()
if page-num > 1 and calc.odd(page-num) {
let headings = query(heading)
let curr-heading = none
let found = false
for heading-elem in headings {
if heading-elem.location() != none and heading-elem.location().page() == page-num {
curr-heading = heading-elem.body
found = true
} else if heading-elem.location() != none and heading-elem.location().page() < page-num {
curr-heading = heading-elem.body // keep track of the last heading on a prev page
} else if found { break } // stop once we have moved past the curr page
}
align(right, emph(curr-heading))
} else { none }
}, // context ends
) // page ends


Some of the above and more are available at my repository, note. And if you’re a Neovim user, then you can use my plugin: typst-snippets-vim.