8 Fragments and Equations
8.1 Placing arbitrary content
typ.place(x, y, body, align: center + horizon)
typ.place(point, body, align: center + horizon)Places any Typst content at a diagram coordinate and folds its measured box into the diagram’s bounds, the same as a node would. Alignment must be physical — left/center/right and top/horizon/bottom — rather than logical start/end, because diagram coordinates are physical and direction-independent; there’s no surrounding text direction for start/ end to resolve against.
For a caption that should follow a gate port, use typ.place(typ.offset(p, 0, -0.5), [caption]), or use p.x/p.y for only one relative axis. See Relative positioning.
place() content keeps its own size under both diagram(scale:) and group(scale:): only its position moves. To resize a caption, set its text size or wrap the content in Typst’s own scaling function. Supplying just one alignment axis centers the unspecified axis.
8.2 Reusable fragments with group()
A “fragment” is nothing more than a diagram-item array that hasn’t been handed to diagram() yet — which means it’s an ordinary Typst value. Define one once with a plain let, and place it as many times as you like:
#let motif = {
let c = typ.node(0, 0)
let t = typ.node(0, -1)
typ.edge(c, t)
}
#diagram({
typ.group(motif) // as-is
typ.group(dx: 2, scale: 0.6, motif) // moved and shrunk
})typ.group(dx: 0, dy: 0, scale: 1, rotate: 0deg, pivot: (0, 0), ..items)group() applies one affine transform to a fragment (or several — items can be a mix of raw diagram items and nested group() calls, since both are just item arrays):
- Translation and scaling move and resize everything: positions, deferred
rel()vectors, Bézier controls,bend:amounts, gate-port attachment directions, andfrom:/to:handle directions and strengths all follow. Scaling additionally changes node shapes, label size, edge strokes, and highlights — a motif built at a comfortable working size still looks proportionally right stamped down smaller. - Rotation turns positions and edge geometry about
pivot, but deliberately leaves node graphics, labels, andplace()content upright, so nothing becomes harder to read just because its container fragment rotated. Give an affected node its ownstyle: (rotate: -angle)if its shape should visually track the group’s rotation — note the sign flip, sincegroupangles are the diagram’s mathematical convention while a node’s ownstyle.rotateis Typst’s screen convention (see Shape Builders).
#diagram({
typ.group(motif)
typ.group(dx: 1.4, motif) // the same gadget, reused, side by side
})For a fragment that should vary between placements — different labels, different sizes — make it a function instead of a bare value:
#let pair(a, b) = typ.edge(
(-0.7, 0), typ.node(0, 0, label: a), typ.node(1, 0, label: b), (1.7, 0),
)
#diagram({
typ.group(pair($alpha$, $beta$))
typ.group(dy: -1.1, pair($pi/2$, $gamma$))
})8.2.1 Names and reusable fragments
Nodes are values, not mutable handles. group() returns transformed copies without changing the original fragment. Equal node values emitted more than once are drawn once, including nodes captured by multiple edges.
Names are unique across the entire diagram; group() does not rename them or introduce a namespace. Reusing a named fragment at a different position therefore causes a duplicate-name error. Give a fragment factory a prefix:
#let named-pair(prefix) = {
typ.box(0, 0, label: [A], name: prefix + "-a")
typ.box(1, 0, label: [B], name: prefix + "-b")
typ.edge(typ.ref(prefix + "-a"), typ.ref(prefix + "-b"))
}
#typ.diagram({
named-pair("first")
typ.group(dy: -1, named-pair("second"))
})A ref() resolves against the final diagram’s emitted nodes, including nodes declared later. It does not capture or emit a node itself. Prefer direct node values for self-contained fragments that do not need external named access.
Relative node/content positions also transform with a fragment. Projections such as p.x and p.y select the fragment’s local axes: after a quarter-turn, an original x-alignment becomes a vertical alignment. Whole-point placement still lands on the same actual port as an edge endpoint, including when the gate’s graphic stays upright. Named references remain global anchors; they are not translated again just because the referring fragment moves.
8.3 Diagrams inside equations
diagram() returns a box whose y = anchor line (anchor: defaults to 0) is aligned to the math axis, so a diagram embedded in an equation lines up with the relation symbols and with any other diagram in the same equation — even a taller one — regardless of what content sits above or below that main wire:
#let lhs = diagram(scale: 0.65cm, {
typ.edge(typ.node(0, 0, label: $alpha$), typ.node(1, 0))
})
#let rhs = diagram(scale: 0.65cm, {
typ.edge(typ.node(0, 0), typ.node(1, 0))
})
$ #lhs = 2 dot lr(( #rhs )) $Prefix an embedded diagram with # inside $ .. $ — a bare identifier only parses correctly if its name has no hyphen, since a hyphen inside math is a minus sign (fuse-lhs would parse as fuse - lhs), so # is the form that always works regardless of naming. Use lr(( .. )) (or any other lr() delimiter pair) for brackets that should grow to the diagram’s height.
If a diagram’s main wire isn’t at y = 0, name the actual line with anchor: so it — not the diagram’s geometric centre — is what aligns to the math axis:
#let two-wire(a) = diagram(scale: 0.55cm, anchor: a, {
typ.edge((0, 0), (1.6, 0))
typ.edge((0, 1), (1.6, 1))
})
$ #two-wire(0) != #two-wire(1) $math-axis (normally 0.25em) and baseline (normally computed, but overridable directly) are the two lower-level knobs this alignment is built from — see the Diagram Reference if the computed placement ever needs a manual nudge.