3MW (Creating Beautiful PDF Reports with Typst and Quarto)

Enjoy 3 Minute Wednesdays at no cost.

This newsletter is brought to you for free. If you want to advance your R skills and support my work at the same time, then you will like my paid offerings:

Guten Tag!

Many greetings from Munich, Germany. Have you ever heard of the Typst project? It is a lightweight, Markdown-inspired language designed for creating beautiful, customized PDF documents with minimal effort.

Basically, it is supposed to be like LaTeX but muuuch easier. And the coolest part? You can use Typst from within Quarto to create stunning PDF reports.

So in today’s newsletter, I’ll walk you through how to style our PDF document from last week. Let’s dive in

Set Typst as Your PDF Format

To start using Typst from Quarto, go into your .qmd file and specify typst as the output format in the YAML header:

If you render this now, you’ll get a similar PDF output as before.

Add Custom Styling with Typst Templates

To make our PDF look good, we’ll use template partials provided by Typst. There are two key files that we need to mention in the YAML header for that:

Both are plain text files that you can create by just taking a .txt file and changing the extension to .typ. So the more important question is: “How do we fill these files?” Let’s go through both files.

Filling typst-show.typ

This file defines the variables that are passed into the layout, such as the title, author, and parameters from your Quarto YAML. You see, what you you define in the Quarto YAML header isn’t automatically known to Typst. That’s why you need to transfer it. For that you use the #show function from Typst.

There, you specify that your “article” layout (that’s the default name of the used layout) is equipped with a set of variables.

The syntax takes some getting used to. Basically, you always write

Yeah, I know. As I said, it takes some getting used to. And for drilling down nested YAML structures, you can just use a dot symbol (.). With that we know enough Typst code to transfer title, author and params.species from our Quarto YAML header to Typst.

Filling typst-template.typ

Nice. One file down, one more to go.

In the typst-template.typ file, we define the actual article layout of our PDF report. For that, we use the #let function.

Inside the function body (indicated by {}), we can now set attributes of

  • document (contains metadata),

  • text (defines text properties of the document), and

  • page (defines the properties of every page in the document).

Have a look at the code and see if you can make out what it does. I like to think that it’s somewhat manageable to piece together what it does. But don’t worry. I’ll explain afterwards anyway.

So here, we have used the set statement to specify properties of the document, text and page objects. There, we’ve set some of the objects attributes (like title, font or paper) to values that were

  • either hard-coded (like "Source Sans Pro" or the margins) or

  • filled using arguments of the #let() function (like author or paper).

Add a page

So far so good. We’ve set a couple of things. But we haven’t added any content to the layout yet. Let’s change that.

Let’s insert a page using the page() function. In there, we can make sure that everything is left-aligned and starts at the top of the page using align(). The syntax for that is

Add content

See that line that says “page content goes” here? That’s where we can place the content of the page. Here, let’s just try to use the body variable from the article() function to insert everything that is inside our Quarto document.

That should be a valid layout. Let’s try to render this. Here’s what we’ll get:

Oh no. That’s a bit dumb. Instead of understanding body as variable, Typst things that we actually want to use a literal text. But we have used other variables just like that before, haven’t we? What’s going on?

Markdown vs evaluation context

You see, we are currently filling the align() function inside the page() function with content. This content is inside brackets [] where it is evaluated as Markdown. Not in any functional kind of way. And the magical signal for Typst to switch to an evaluated context is #.

That’s also why the let() function to define the article layout started with #. Otherwise, Typst would think that our file is just filled with plain text and won’t evaluate anything. And the same has do be done now that we are inside a Markdown content block inside the brackets of align(). So, let’s try this:

Ahh much better. There’s all of our content. And notice how everything fits on one page now.

Add our parameters

Similarly, we can add our parameters using the text() function. The syntax is the same as with align():

  • First, we fill text() with arguments like text(size:1.6em).

  • Then, we create actual content inside the brackets like `text(size: 1.6em)[content].

  • And finally we have to use # inside the Markdown content blocks to use functions and variables.

This will give us this terrible look.

Let’s add some vertical space with the v() function.

And then we have way more space:

Ahh much better. Let that sink in for now. Next week, I’m showing you how to add colors to our designs.

And as always, if you have any questions, or just want to reach out, feel free to contact me by replying to this mail or finding me on LinkedIn or on Bluesky.

See you next week,
Albert 👋

Enjoyed this newsletter? Here are other ways I can help you:

Reply

or to participate.