Skip to content

Lesson 3 - Templating

Components

Components aren't a Typst concept, but they're probably a good way to think about functions and, more generally, about development. For example, let's look at this code:

#align(horizon, stack(
  dir: ltr,
  spacing: 0.3cm,
  circle(fill: rgb("#2a9d8f"), width: 0.7cm),
  text(fill: rgb("#2a9d8f"), "Ready"),
))

#align(horizon, stack(
  dir: ltr,
  spacing: 0.3cm,
  circle(fill: rgb("#e9c46a"), width: 0.7cm),
  text(fill: rgb("#e9c46a"), "Pending"),
))

#align(horizon, stack(
  dir: ltr,
  spacing: 0.3cm,
  circle(fill: rgb("#e76f51"), width: 0.7cm),
  text(fill: rgb("#e76f51"), "Blocked"),
))

Three status rows showing Ready, Pending, and Blocked with matching colored circles

The code is highly duplicated! Only the label and the color are changing in each element. Instead, what we want to do is:

#let custom-component(lab, col) = {
  align(horizon, stack(
      dir: ltr,
      spacing: 0.3cm,
      circle(fill: col, width: 0.7cm),
      text(fill: col, lab)
   ))
}

#custom-component("Ready", rgb("#2a9d8f"))
#custom-component("Pending", rgb("#e9c46a"))
#custom-component("Blocked", rgb("#e76f51"))

Three status rows showing Ready, Pending, and Blocked with matching colored circles

This makes our code:

  • highly reusable: we can call the custom-component function many other times
  • easier to maintain: change the function definition to update all of its uses

Typst is really well-designed for this component mindset, and it's a good idea to use it from the beginning.

set and show rules

Our previous example could also be made using set rules (which we already saw in the first lesson). As a reminder, a set rule defines the default behavior of a function. For example, if I write:

#set circle(width: 1cm)

Then, by default, all circles will have a width of 1cm. If we take our example from before:

#set stack(dir: ltr, spacing: 0.3cm)
#set circle(width: 0.7cm)

#align(horizon, stack(
  circle(fill: rgb("#2a9d8f")),
  text(fill: rgb("#2a9d8f"), "Ready"),
))

#align(horizon, stack(
  circle(fill: rgb("#e9c46a")),
  text(fill: rgb("#e9c46a"), "Pending"),
))

#align(horizon, stack(
  circle(fill: rgb("#e76f51")),
  text(fill: rgb("#e76f51"), "Blocked"),
))

Three status rows showing Ready, Pending, and Blocked with matching colored circles

Typst also has a more advanced version of set rules called show rules. Basically, show rules let us define what happens when a function is called. Let's see an example:

#show heading: set text(fill: blue)

== Hey you
Content of the document

== Another section
And the rest of the document

Document with two blue section headings and black body text

Here, our show rules say: when adding a heading, set the text to blue by default. Very practical! But what if we want to apply a different color depending on the heading level?

#show heading.where(level: 1): set text(fill: green)
#show heading.where(level: 2): set text(fill: blue)

= Hey you
== Section
And the rest of the document

Document with a green level-1 heading, a blue level-2 heading, and black body text

But we can even go much further with show rules! For example:

#show heading: it => align(right)[
  #text(weight: "bold", [_The heading is_: "#it.body" (level: #it.level)])
]

= Hey you
== Section
And the rest of the document

Right-aligned text describing each heading and its level

In short: we can do whatever we want! Here, it represents our heading:

  • it.body: the content
  • it.level: the heading level
  • it.*: this works for all other arguments in the heading function (and for all functions too)!

Show rules differ from set rules by the fact that they don't just control default arguments of a function, but how that function will behave when called, which is an extremely powerful mechanism.

One thing where Typst shines is minimizing code duplication: it's very rare that you need to write the same code twice, and it's often a symptom that your code should be refactored.

A good example is adding a header or footer to a Typst document.

Note

Since headers and footers behave exactly the same in Typst, we'll only showcase how to create a header here.

In order to have a header, we need to call a set rule on the page() function and specify the header argument:

#set page(margin: 1cm, header: {
  rect(fill: lime, height: 1cm, width: 100%, "This is the header")
})

= Title of the document
Some text goes here...

#pagebreak() // forces a new page

= Another title

Two-page document with a lime header bar reading "This is the header" on each page

Context

An important thing in Typst is the context. It is a keyword that can give us information about the current context. For example:

  • what is the page number of the current page?
  • what is the language of the document?
  • what is the text color?
  • And much more!

The main use case for context is adding the page number to the header or footer:

#set page(
  margin: 1cm,
  height: 3cm,
  width: 15cm,
  header: {
    rect(
      fill: lime,
      height: 1cm,
      width: 100%,
      [#context [This is the page #here().page() (out of #counter(page).final().at(0))]],
    )
  },
)

= Title of the document
Some text goes here...

= Another title

= Yet another title

Three-page document with a lime header bar showing the current page number out of 3

Let's summarize the key parts here:

  • Everything is wrapped in #context []. This is required; otherwise, the next function calls won't work.
  • #here().page() gives us the current page number (either 1, 2 or 3).
  • #counter(page).final().at(0) gives the total number of pages in the entire document (always 3).

Info

context can do even more complex things (such as counting the number of headings before its call), but this will not be detailed here as this concerns relatively few users.



Question

Having questions? Feedback? Feel free to ask!