7 Configuration and Scoping
Setting a default once instead of repeating it on every diagram() call has three different mechanisms in typograph, because #set — Typst’s own scoped-default syntax — is reserved for element functions, and diagram isn’t one. #set typ.diagram(..) is a hard compile error, and no package can change that. The three mechanisms cover the same ground #set would have:
7.1 1. diagram.with(..) — the idiomatic default
Typst’s own partial application, free at runtime:
#let diagram = typ.diagram.with(theme: my-theme, scale: 0.8cm, font-size: 9pt)Bind this once near the top of a document (or a project’s shared theme file) and call diagram({ .. }) everywhere after. A let-shadowed rebind later in the document gives scoped defaults for free, following normal Typst scoping — this is the right default for anything the author of the document controls directly.
7.2 2. #set text(..) — already works
Node and edge labels are set in 1em and inherit the surrounding text style, so #set text(size: 9pt) (or any other text property) affects diagram labels exactly as it affects running prose, with no package involvement at all.
7.3 3. config() — for call sites you don’t control
typ.config(
body,
scale: .., scale-edges: .., font-size: .., grid: .., inset: ..,
anchor: .., math-axis: .., baseline: ..,
node-styles: .., edge-styles: ..,
)config() supplies these non-theme defaults to every diagram() inside body that doesn’t override the same argument itself. It exists for the one case .with() can’t reach: diagrams inside content you don’t directly control the call sites of — an #included chapter, or a template applied #show: template.with(..) over a whole document:
#show: typ.config.with(font-size: 9pt, scale: 0.8cm) // whole documentor scoped to one section:
#typ.config(font-size: 7pt)[
... diagrams here are small ...
]Scopes nest and revert: a nested config() overrides its parent for its own body, then reverts cleanly once that body ends, and a diagram’s own explicit argument always wins over any configured default, at any nesting depth.
#typ.config(font-size: 8pt, scale: 0.6cm)[
Small throughout this scope:
#diagram({ typ.edge(typ.node(0, 0, label: $alpha$), typ.node(1, 0)) })
#typ.config(font-size: 13pt, scale: 1cm)[
A nested scope overrides it:
#diagram({ typ.edge(typ.node(0, 0, label: $alpha$), typ.node(1, 0)) })
]
...and reverts afterwards:
#diagram({ typ.edge(typ.node(0, 0, label: $alpha$), typ.node(1, 0)) })
]node-styles/edge-styles are the one place config() does more than plain override: they merge with whatever the package/theme already contributed rather than replacing it wholesale — node-styles merges one level by kind, edge-styles merges shallowly — and a diagram’s own node-styles:/edge-styles: then merges on top of the configured ones, the same “later wins, per key” rule as everywhere else in the style system (see Core Concepts).
theme is deliberately not a config() option, for the same reason it isn’t a plain default anywhere else: appearance is always bound explicitly with diagram.with(theme: ..), never inherited implicitly from an enclosing scope.
Implementation note, for anyone curious why this needed state at all when .with() didn’t: config() is a stack held in document state, so nested scopes revert correctly, read during the diagram’s own contextual layout pass — it doesn’t add a separate pass of its own. See Performance Model for the renderer’s actual passes.