Typst Guide

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 caseUse
Page number, heading numbercounter
Custom incrementing number (definitions, exercises)counter
Current chapter titlestate
Accumulating list / dictionarystate
Theme/mode togglingstate
Anything non-numericstate

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.

Try Typst state live

TypeTeX is a free in-browser Typst editor — experiment with state and counters and see updates in milliseconds.

Try TypeTeX free

Frequently Asked Questions

What is #state in Typst?

#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.

How do I create and use a state?

#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()'.

What's the difference between #state and #counter?

#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).

What does #context do?

#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.

When should I use #state?

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.

How do I update state with a function?

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.

Can I store complex data in state?

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.

How do I get the final value of a state?

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.

What's a real-world example of #state?

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.

More Typst guides