Typst State
Track mutable values across your document. #state(name, initial), .update(), .display(). Always wrap reads in #context.
The basics
// Create a state with initial value
#let mood = state("mood", "neutral")
// Read current value (inside #context)
The current mood is #context mood.display().
// Update the state
#mood.update("happy")
// Read again — now sees the new value
The current mood is #context mood.display().
// Update with a function
#mood.update(prev => prev + " and excited")Real-world: track current chapter for header
#let current-chapter = state("chapter", [])
// Update state on every level-1 heading
#show heading.where(level: 1): it => {
current-chapter.update(it.body)
it
}
// Page header shows the current chapter
#set page(
header: context align(right)[
#current-chapter.get()
],
)
= Introduction
... 'Introduction' shows in header on these pages ...
= Methods
... header switches to 'Methods' ...
= Results
... 'Results' in header ...Track a list of definitions
#let definitions = state("defs", ())
#let define(term, body) = {
definitions.update(d => d + ((term: term, body: body),))
block(stroke: 0.5pt, inset: 8pt)[
*#term*: #body
]
}
#define[Algorithm][A finite sequence of well-defined instructions.]
#define[Function][A relation between sets...]
// Print all definitions at the end as a glossary
== Glossary
#context {
for def in definitions.get() {
block(spacing: 0.5em)[*#def.term*: #def.body]
}
}State vs counter — which to use?
| Use case | Use |
|---|---|
| Page number, heading number | counter |
| Custom incrementing number (definitions, exercises) | counter |
| Current chapter title | state |
| Accumulating list / dictionary | state |
| Theme/mode toggling | state |
| Anything non-numeric | state |
Why #context matters
Typst evaluates content somewhat lazily for performance. State values depend on cumulative .update() calls before the read point — so the value at a given position isn't known until Typst processes the document linearly.
#context tells Typst "evaluate this expression with knowledge of my current position." Without it, you'd see something like "state value at unknown location" or get the initial value regardless of updates.
TypeTeX is a free in-browser Typst editor — experiment with state and counters and see updates in milliseconds.
Try TypeTeX freeFrequently Asked Questions
#state is Typst's mechanism for tracking values that change throughout your document — like global variables that respect document order. Unlike regular #let bindings (which are immutable in scope), state can be updated mid-document, and queries to its value see the most recent update prior to the query point.
#let mood = state("mood", "neutral"). Then mood.update("happy") changes the value. mood.display() shows the current value. Wrap mood.display() in #context to evaluate at the current document position: 'The current mood is #context mood.display()'.
#counter is specifically for incrementing numbers (page numbers, heading numbers, figure numbers). #state is for arbitrary values — strings, dictionaries, arrays, anything. Use counter for numeric tracking, state for everything else (current chapter title, theme variables, accumulated lists).
#context defers evaluation until Typst knows the document position. State values depend on the cumulative effect of all #update() calls before that point. Without #context, state queries can't be resolved (Typst doesn't know where you are yet). Always wrap state queries in #context.
Whenever you need a value that changes during document construction and you want to query it later. Examples: tracking which chapter you're in (for headers/footers), accumulating a list of definitions, switching between dark/light theme conditionally, building a custom 'last updated' timestamp.
mood.update(prev => prev + " plus tired") — pass a function that takes the previous value and returns the new value. Useful when the new value depends on the old, like incrementing a custom counter or appending to a list.
Yes. #state can hold any Typst value: arrays (state("list", ())), dictionaries (state("data", (a: 1, b: 2))), even functions. Update with .update((a, b) => ...) for replacing or appending. Common pattern for tracking definitions, exercises, or todo items.
mystate.final() returns the value at the end of the document — useful for footers showing 'last updated' or 'total chapters: N'. Like counter.final(), it requires #context to evaluate. mystate.at(label) returns the value at a specific labeled position.
Tracking the current chapter for a running header: #let current-chapter = state("chapter", ""). #show heading.where(level: 1): it => { current-chapter.update(it.body); it }. Then in the page header: #context current-chapter.display(). The header always shows the current chapter title.