With Typst 0.13, we wanted to improve the day-to-day experience of using Typst. We fixed some of the most long-standing bugs and made Typst even more flexible to use. And on top, we're shipping a first, experimental version of HTML export.
It's been almost two years since Typst's open-source launch and the project has matured quite a bit since then. Typst 0.12's development cycle saw many large-scale changes to Typst's foundations. With Typst 0.13, we moved the focus to the day-to-day experience of using Typst. We made quality-of-life improvements all across and fixed some of the biggest paper cuts. But, of course, we also shipped some exciting new features!
Contents
In this blog post, I'll walk you through the highlights of the release. If you prefer a more visual take on the topic, also check out the release video.
- Paragraphs and first line indent
- Better-looking outlines
- New curves
- Files and bytes
- Generating images
- Faster plugins
- Single-letter strings in math
- Font coverage control
- PDF file embedding
- A first look at HTML export
- Migrating to Typst 0.13
- Community Call
For a comprehensive overview of all changes in the release, visit the changelog. If you're looking to upgrade your document to Typst 0.13, you can also skip ahead to the Migration section.
Paragraphs and first-line indent
The work on semantic paragraphs is what I'm most proud of in this release, but at the same time it's among the things that are least visible for users. What do I even mean with "semantic paragraphs?"
Let me explain. Up until now, Typst considered every piece of text you wrote as a paragraph — be it a single word in a page header, a figure caption, or a page number. Just like paragraphs, these things can have spacing, break across lines, etc. Layout-wise they are not all that different from paragraphs.
However, there are semantical differences. Only proper paragraphs should be counted when paragraphs are numbered (such as in legal texts). Only proper paragraphs should be announced by a screen reader as such. And even layout-wise there are differences; for instance, that only proper paragraphs should have first-line indent. While the layout routines for "just text" and a paragraph may be very similar, the second order effects of something being a proper paragraph are far-reaching.
In version 0.13, Typst finally gains a better understanding of this distinction. Whether something is a paragraph or just text is decided based on a few simple rules about which you can read in the updated paragraph documentation.
The most visible immediate effect of this work is that first-line-indent
can now be applied to all paragraphs instead of just consecutive ones, closing the most upvoted Typst bug. Semantic paragraphs are also crucial for the in-development HTML export and for planned future work on PDF accessibility.
#set block(spacing: 1.2em)
#set par(
spacing: 0.65em,
first-line-indent: (
amount: 1em,
all: true,
),
)
= Chapter 1
In this text, the paragraphs are
all indented, without exception.
With `all: true`, the first
paragraph is affected as well.

Better-looking outlines
If you've created a table of contents with Typst's outline functionality before, you might remember that it always looked a bit bland. The default style had no indentation and rather tightly dotted leaders (leaders are the filler dots between a title and its page number).
In Typst 0.13, the outline gets a full facelift while also becoming easier to customize. The new automatic indentation nicely aligns all titles and numberings across the whole outline, long titles have better-looking wrapping behavior, and we fixed a number of bugs.
If you have a customized outline, check out the migration section for the outline to learn how to adapt your customization.
#set heading(numbering: "1.i.")
#outline()
= Introduction
= Methods
== Experiments
== Statistics
= Results
== T Experiment
== K Experiment
== V Experiment
== Additional experiments with extra length
= Discussion
= Conclusion

New Curves
Since Typst 0.2, you could draw Bézier paths with the path
function. However, the input format of this function was rather arcane. Rather than specifying pen movements as in an SVG, you had to specify directly points with their two control points. Moreover, the path function had a fatal flaw: You could not close a path and then keep on drawing. This is necessary to draw a shape with cutouts, as depicated below.
The new curve
function fixes these flaws. It provides an easier-to-understand and more expressive interface. We also used this opportunity to change the name from path
to curve
as we plan to repurpose the name path
for a file path type in an upcoming release.
#curve(
fill: blue.lighten(80%),
fill-rule: "even-odd",
stroke: blue,
curve.line((50pt, 0pt)),
curve.line((50pt, 50pt)),
curve.line((0pt, 50pt)),
curve.close(),
curve.move((10pt, 10pt)),
curve.line((40pt, 10pt)),
curve.line((40pt, 40pt)),
curve.line((10pt, 40pt)),
curve.close(),
)

Thanks to @Emm54321 for working on this!
Files and bytes
Various functions in Typst load files, be it images, data loading functions, or plugins. Sometimes though, a little extra flexibility is needed, for example, to preprocess, generate, or inline data into a Typst document.
For this reason, there are also .decode
variants on various of the functions, e.g. image.decode
or json.decode
. However, that approach didn't work so well when a path is expected in a set rule, as in set raw(theme: "light.tmTheme")
. It also introduced duplication: All the properties of an image are also spelled out again in image.decode
.
Typst 0.13 revamps file handling to improve this unsatisfactory situation. All places where a path is expected now also support raw bytes instead. Typst will always interpret a string as a path and raw bytes as data. When trying to decode an image from a string, thus make sure to first convert it to bytes. Converting to bytes is cheap as Typst will internally reuse the memory from the string. It will even remember that the bytes came from a string to make conversions back to a string cheap as well!
The existing .decode
functions are now deprecated as they are not needed anymore. The .encode
variants of data loading functions remain unchanged.
See @netwok for details.
#bibliography(bytes(
```bib
@article{netwok,
title={At-scale impact of the {Net Wok}},
author={Astley, Rick and Morris, Linda},
journal={Armenian Journal of Proceedings},
volume={61},
pages={192--219},
year={2020},
publisher={Automattic Inc.}
}
```.text
))

Generating images
With the new byte-taking image
function (and previously image.decode
), you can generate images at runtime. However, the image function expects images in an encoded image exchange format like PNG or JPEG. Producing valid bytes for such a format in pure Typst code is prohibitively complicated. Meanwhile, plugins are unnecessarily bloated and slowed down if they have to include an image encoder.
To streamline image generation workflows, Typst 0.13 thus brings support for loading images from uncompressed raw pixel data. To that end, the format
parameter of the image function supports a new form, where the channel encoding and pixel width/height of the image can be specified. This feature is crucial for better scientific visualizations — think things like heatmaps.
#image(
bytes(range(64).map(x => x * 4)),
format: (
encoding: "luma8",
width: 8,
height: 8,
),
width: 4cm,
scaling: "pixelated",
)

Thanks to @frozolotl for working on this!
Faster plugins
In version 0.8, Typst gained support for WebAssembly plugins — one of the features that would very likely still be a little blue "feature request" label if not for our fabulous open source community. Since then, plugins have become the backbone of various community packages. They're great because they bring the power and package ecosystem of all the languages that compile to WebAssembly right into Typst.
They are also faster to execute than Typst code. Still, with heavy usage the time spent executing plugin code can make up a significant chunk of compile time. A simple way to improve this would've been to switch to a faster WebAssembly runtime (specifically, from wasmi
to wasmtime
). However, taking on a dependency on a WebAssembly runtime with just-in-time compilation1 wasn't a spot we wanted to put Typst into. It would have reduced portability and security and increased the amount of third-party code Typst depends on by a lot.
There was another way to speed up plugins: Since 0.12, Typst's layout engine is multi-threaded. Plugins didn't profit from this though as they couldn't be easily replicated across threads. This is a limitation we're lifting with 0.13. Typst will now automatically run plugins in multiple threads without any changes from plugin authors. This is possible because we require (and also already required in the past) plugin functions to be pure. This means that we can execute plugin functions out of order without a visible effect. For cases where purity is too limiting, Typst 0.13 introduces the new plugin.transition
API, which lets plugin authors deal with stateful operations in a sound way.
The work on speeding up plugins was prioritized through a Typst open-source support contract. If you're using Typst in production at your company and are hitting any road blocks, please reach out to us!
Single-letter strings in math
Since Typst's initial release, $ "hi" $
would generate the letters "h" and "i" in upright style while $ "h" $
would result in an italic "h". It's one of the longest-standing bugs, which is curious because it seems so easy to fix. Unfortunately, it was not. To see why, we need to take a look behind the scenes and understand how Typst views your equations.
In Typst 0.12 and lower, the x
in $ x $
is a text element like any other text in your document. A string like "hi"
is converted to content by becoming such a text element, too. While different syntactically, $ x $
and $ "x" $
thus used to yield identical content. Since x
becomes italic by default, "x"
did, too.
Changing that by itself wouldn't have been too hard, but there is a third guest to the party: Symbols. The pi
in $ pi $
is a symbol value. Like strings, symbols were up until now converted to content by becoming text elements. This means they work both in math and normal text (as #sym.pi
).
For a long time, the issue was thus blocked on finding a general solution to the text element ambiguity — perhaps introducing a new var
element for math-y text. That story isn't fully written yet, but for 0.13 we really wanted to fix the issue at hand. For this reason, we attempted to find a minimal solution that fixes the issue while leaving our options for further improvements open.
The solution we came up with: Bare letters and symbols are now converted into an internal symbol element, which has auto-italics in math, but is transparently converted to normal text outside of math. Meanwhile, strings still generate text elements. With the planned unification of types and elements, this symbol element and the existing symbol type will naturally merge into one.
$ a != "a" $

Thanks to @wrzian for working on this!
Font coverage control
When text in different writing scripts is mixed, it's often important to have precise control over which text is typeset with which font. For example, Latin and Chinese text are almost always typeset with different fonts.
This is quite problematic for CJK (Chinese, Japanese, Korean) Typst users which often have text that mix their native language and English. Typst 0.13 takes a first step to improve this situation. With the new covers
functionality, users can specify precisely for which character ranges a font should be used. This can, for example, be used to define in which font punctuation (which is present in both Latin and CJK fonts) is rendered.
The covers
feature supports specifying a character set, either as a regular expression or one of the built-in ones. Currently, the only built-in set is "latin-in-cjk"
, which should be specified for a Latin font that is before a CJK font in the fallback list. In the example below, we can put Inria Serif first in the fallback chain while still having quotes render with Noto Serif CJK SC.
// Mix Latin and CJK fonts.
#set text(font: (
(
name: "Inria Serif",
covers: "latin-in-cjk",
),
"Noto Serif CJK SC"
))
分别设置“中文”和English字体

Thanks to @peng1999 for working on this!
PDF file embedding
With Typst 0.13's new pdf.embed
function, you can attach arbitrary text or binary files to your PDF. These embedded files can then be browsed in PDF viewers or extracted programmatically by third-party tools.
When is this useful? One example, electronic invoicing, is ever more important as new EU legislation just came into force. While electronic invoices are typically in XML-based formats, it's often useful to still have a human-readable and printable invoice.
With PDF file embedding, the XML invoice data can be inserted into the PDF itself, forming a hybrid invoice. Currently, there is still one missing piece in Typst's support for this: The PDF metadata must identify the document as an E-Invoice to other applications. We plan to add support for embedding arbitrary metadata like this in a future Typst release.
Thanks to @NiklasEi for working on this!
A first look at HTML export
Saved for last is a particularly exciting topic: We've been starting work on HTML export! The feature is still very incomplete and only available for experimentation behind a feature flag, but there's already some stuff to see.
What works
Most of the markup and some of the other built-in functions like figure
and table
already produce the appropriate HTML. Our focus is on producing semantically rich HTML that retains the structure of the input document. The HTML output should be accessible, human-readable, and editable by hand.
The Typst document below
= Introduction
A *document* with some _markup._
- A list
- with elements
#figure(
[A bit of text],
caption: [My caption],
)
will produce the following HTML output:
<!-- html, head, and body omitted for brevity --> <h2>Introduction</h2> <p>A <strong>document</strong> with some <em>markup.</em></p> <ul> <li>A list</li> <li>with elements</li> </ul> <figure> <p>A bit of text</p> <figcaption>Figure 1: My caption</figcaption> </figure>
Typst cannot always produce the perfect HTML automatically. Instead, it gives you full control by letting you generate raw HTML elements:
#html.elem(
"div",
attrs: (style: "background: aqua")
)[
A div with _Typst content_ inside!
]
<div style="background: aqua"> A div with <em>Typst content</em> inside! </div>
To make your document portable across PDF and HTML, we're also introducing a target
function that returns the current export format (either "paged"
or "html"
). It is mainly intended for use in show rules, like below:
#let kbd(it) = context {
if target() == "html" {
html.elem("kbd", it)
} else {
set text(fill: rgb("#1f2328"))
let r = 3pt
box(
fill: rgb("#f6f8fa"),
stroke: rgb("#d1d9e0b3"),
outset: (y: r),
inset: (x: r),
radius: r,
raw(it)
)
}
}
Press #kbd("F1") for help.

<p>Press <kbd>F1</kbd> for help.</p>
The target
function is contextual because the export target can vary within one compilation. How? With the html.frame
function, you can lay out part of your HTML document as an inline SVG, using Typst's normal layout engine. Within such a frame, the compilation target is "paged"
again, so that show rules produce the appropriate layout elements instead of HTML elements.
What's missing
A lot! For instance, currently, Typst will always output a single HTML file. Support for outputting directories with multiple HTML documents and assets, as well as support for outputting fragments that can be integrated into other HTML documents is planned.
Typst currently also doesn't output any CSS, instead focusing fully on emitting semantic markup. You can of course write your own CSS styles and still benefit from sharing your content between PDF and HTML.
In the future, we plan to give you the option of automatically emitting CSS, taking more of your existing set rules into account. More generally, we have a lot of plans for HTML export! Visit the tracking issue to learn more about them.
Try it!
In the CLI, you can experiment with HTML export by passing --features html
or setting the TYPST_FEATURES
environment variable to html
. In the web app, HTML export is not yet available. It will become available once we think it's ready for general use.
You can also use HTML export with typst watch
. Typst will then automatically spin up a live-reloading HTTP server that serves your document.
$ typst watch hi.typ --features html --format html --open watching hi.typ writing to hi.html serving at http://127.0.0.1:3001 [20:31:09] compiled with warnings in 1.52 ms
Sponsorship
Work on Typst's HTML export is sponsored by NLNet. We are very grateful for their support! We also want to thank external contributor @01mf02 with whom we've thus far collaborated on HTML export through the NLNet grant. Unfortunately, we and him have since parted ways over technical differences. Nonetheless, we plan to increase the time and resources we put into HTML export, and we are very happy to have NLNet's continued support in this endeavor.
Migrating to Typst 0.13
Typst 0.13 ships with a number of deprecations and breaking changes. The changelog has a full account of all changes, but in this section you'll learn how to deal with the most common kinds of breakage.
Type/string compatibility
In Typst 0.8, types were promoted to proper values. As a result, type(10)
directly returns a type instead of a string since then. To make this change less disruptive, we also introduced a temporary compatibility behavior where types could be used like strings in some contexts (e.g., int == "integer"
would be true). For implementation reasons, we did not add a warning for this at the time. We're rectifying this now and adding a warning to shake out remaining reliance on this behavior. With Typst 0.14, the compatibility behavior will be fully removed.
// Typst 0.8+ ✅
#{ type(10) == int }
// Typst 0.7 and below ⚠️
#{ type(10) == "integer" }
Decode functions
The image.decode
function and the .decode
variants of data loading functions are now deprecated. You can instead directly pass bytes to the respective top-level functions instead. Read the section on files and bytes to learn more.
// Typst 0.13+ ✅
#image(bytes("<svg>..</svg>"))
// Typst 0.12 and below ⚠️
#image.decode("<svg>..</svg>")
Outline
The changes to the built-in outline (table of contents) improve the out-of-the-box style and customizability. Unfortunately, they also break some existing outline customizations.
First of all, the fill
argument moved from outline
to outline.entry
. If you get the error "unexpected argument: fill", adjust your code as shown below:
// Typst 0.13+ ✅
#set outline.entry(fill: none)
// Typst 0.12 and below ❌
#set outline(fill: none)
Because the fill
property is now on the entry, it can also be configured for individual outline levels, like this:
#show outline.entry.where(level: 1): set outline.entry(fill: none)
In light of the changes to paragraphs, outline entries now show themselves as blocks instead of lines of text. This means spacing is now configured via normal show-set rules for block.spacing
.
// Typst 0.13+ ✅
#show outline.entry.where(level: 1): set block(below: 1em)
// Typst 0.12 and below ⚠️
#show outline.entry: it => {
it
v(1em, weak: true)
}
In Typst 0.12 and below, outline entries expose a few fields like .body
and .page
that are useful for writing an outline entry show rule. These fields were derived from other fields for your convenience. Typst 0.13 makes this more explicit and idiomatic by turning them into methods. Read the documentation on outline customization for more details on how to use these methods.
// Typst 0.13+ ✅
#show outline.entry: it => {
...
it.page()
}
// Typst 0.12 and below ❌
#show outline.entry: it => {
...
it.page
}
Paths
The path
function is superseded by the new curve
function. The curve
function has an easier-to-understand interface that's closer to how SVG and the HTML canvas work. Read the curve
function's documentation to learn how to express existing paths with the new function.
// Typst 0.13+ ✅
#curve(
fill: blue.lighten(80%),
stroke: blue,
curve.move((0pt, 50pt)),
curve.line((100pt, 50pt)),
curve.cubic(none, (90pt, 0pt), (50pt, 0pt)),
curve.close(),
)
// Typst 0.12 and below ⚠️
#path(
fill: blue.lighten(80%),
stroke: blue,
closed: true,
(0pt, 50pt),
(100pt, 50pt),
((50pt, 0pt), (40pt, 0pt)),
)

Patterns
The pattern
type was renamed to tiling
. To migrate, simply replace pattern
with tiling
. No further changes are necessary. The name pattern
remains as a deprecated alias in Typst 0.13, but will be removed in an upcoming release.
Why the rename? For once, the name pattern
was very generic. The name tiling
is more closely associated with what it expresses. Secondly, we're considering to repurpose the name pattern
for what today are selectors once elements and types are unified.
// Typst 0.13+ ✅
#rect(fill: tiling(..))
// Typst 0.12 and below ⚠️
#rect(fill: pattern(..))
Removals
We also removed a number of things that were already deprecated and warned for in Typst 0.12. This includes the
style
function andstyles
argument ofmeasure
; use a context expression insteadstate.display
function; usestate.get
insteadlocation
argument ofstate.at
,counter.at
, andquery
- compatibility behavior where
counter.display
worked without context - compatibility behavior of
locate
Refer to the Migration section of the Typst 0.12 announcement post to learn more about how to migrate away from these functions.
Community Call
That's it for Typst 0.13. We hope you're just as excited about the release as we are!
We're hosting a community call on Discord on Friday, March 7th. Join us to share your experiences with the new version and to chat with the community!
Just-in-time compilation means that the WebAssembly is compiled to native instructions for your CPU at runtime. While WebAssembly engine authors put a lot of work into making this safe, it's fundamentally more complex, less portable, and more susceptible to exploits than interpreting the WebAssembly code.