2 Core Concepts
Everything else in this guide is a variation on three ideas: how a diagram body collects the things you draw, how those things get their final style, and how coordinates map onto the page. Understanding these up front makes the rest of the API predictable instead of memorized.
2.1 A diagram body is just a list of items
node(0, 0), edge(a, b), and place(0, 0, [hi]) don’t draw anything by themselves — each returns a length-one array containing a tagged dictionary ((type: "node", ..), (type: "edge", ..), (type: "content", ..)). A Typst code block automatically joins the values of its statements with +, and arrays concatenate under +. So this:
#diagram({
typ.node(0, 0)
typ.node(1, 0)
})is really building one array of two node items, the same as writing typ.node(0, 0) + typ.node(1, 0). diagram() just receives that array and lays out whatever is in it — nodes, edges, and content, in any mixture, contributed by any combination of bare statements, let-bindings, loops, or function calls that themselves return item arrays.
This is also why an edge draws its own endpoints: edge(a, b) returns (edge-item, a, b), not just the edge, so the connected nodes come along for free. A node with no edge touching it has nothing to carry it into that sum, so it must appear as its own statement:
#diagram({
let k = typ.node(0, 0, label: $k$) // binding it alone draws nothing
k // this bare statement is what draws it
})Identical complete node records — same coordinate, kind, label, style, name, scale, and metadata — are deduplicated, so building the same node twice (for instance, once while defining an edge and once while defining another) never doubles it up. A non-none node name: must be a string and must be unique within one diagram; ref(name) looks that name up without emitting it.
2.2 Coordinates
Diagram coordinates use the mathematical convention: x increases to the right, y increases upward. This is the opposite of Typst’s own screen convention, and of most other Typst graphics packages. It is deliberate — diagrams are usually sketched and reasoned about on paper with y going up, and every coordinate you write in a typograph document should match that mental model without a mental flip. (The one place the flip actually happens is documented in Architecture, for anyone extending the renderer itself.)
2.3 diagram() at a glance
typ.diagram(
body,
theme: typ.neutral-theme,
scale: auto, scale-edges: auto,
grid: auto, font-size: auto, inset: auto,
node-styles: auto, edge-styles: auto,
anchor: auto, math-axis: auto, baseline: auto,
)themeis the one explicit, atomic appearance value — see Theming.scaleis the coordinate unit and overall zoom: node sizes, labels, and strokes all scale with it.scale-edgesinstead stretches only the coordinate grid, so wires lengthen without enlarging nodes — see Sizing in the reference.node-styles/edge-stylesare this diagram’s own override layer, keyed by node kind (fornode-styles) or applied to every edge (foredge-styles) — the last stop in the precedence chains below.grid,font-size,inset,anchor,math-axis, andbaselineare covered in the reference and in Configuration and Scoping, which is where you’d set most of them once for a whole document instead of per call.
Anything left auto inherits from the nearest config() scope, then the built-in defaults. theme is deliberately not one of those scoped defaults — you always bind it explicitly with diagram.with(theme: ..), so a document can never accidentally inherit an appearance from a scope it didn’t ask for.
2.4 Style resolution: the same idea, twice
Every node style key and every edge style key is resolved by merging dictionaries from lowest to highest precedence, later always winning over earlier. Node styles resolve as:
typ.node-defaults
-> constructor base-style (from node-type(), see Drawing Nodes)
-> selected theme's preset for the node's kind
-> diagram(node-styles: (kind: (...)))
-> node(style: (...)), and direct box()/gate()/map() arguments
and edge styles resolve the same way, with one extra layer for named presets:
typ.edge-defaults
-> theme.edge-defaults
-> edge-type base-style (from edge-type(), see Drawing Edges)
-> theme.edge-presets[preset]
-> diagram(edge-styles: (...))
-> edge(style: (...))
-> direct stroke:/highlight:/clip: arguments
Reading either chain top to bottom is reading “how specific is this override”: core package defaults first, then whatever a reusable constructor baked in, then the theme’s opinion, then per-diagram overrides, then the individual call site — down to a direct stroke: argument, the most specific override there is.
One asymmetry is intentional: node styles are open, so a custom builder can read its own custom keys out of the merged style; edge styles are closed, because there is no edge-rendering extension point to justify the ambiguity — an unrecognized edge style key is always an error. Both chains, and every key in them, are covered exhaustively in the Node and Edge reference pages.