CeTZ Diagrams
CeTZ is a drawing library for Typst with an API inspired by TikZ and Processing. Reference: https://cetz-package.github.io/docs/
Basic Structure
#import "@preview/cetz:0.5.0"
#cetz.canvas({
import cetz.draw: *
// Drawing commands here
})
Draw functions are imported inside the canvas block scope because some override Typst's built-in functions (e.g., line).
Canvas Options
// Default: 1 unit = 1cm
#cetz.canvas({...})
// Custom unit length
#cetz.canvas(length: 0.5cm, {...})
// Scale to parent width
#cetz.canvas(length: 50%, {...})
Canvas auto-sizes to fit content (no fixed width/height).
Coordinate Systems
Cartesian (XYZ)
// 2D coordinates
(2, 3) // x=2, y=3
(x: 2, y: 3) // explicit form
// 3D coordinates
(1, 2, 0.5) // x, y, z
// With units
(10pt, 2cm)
Y-axis points upward (positive direction).
Previous Coordinate
line((0, 0), (1, 1))
line((), (2, 0)) // () references last point (1, 1)
Initial previous coordinate is (0, 0, 0).
Relative Coordinates
// Offset from previous position
line((0, 0), (rel: (1, 1)))
// Non-updating relative (doesn't move "previous")
line((0, 0), (rel: (1, 0), update: false), (rel: (0, 1)))
Polar Coordinates
// angle and radius from origin
(angle: 45deg, radius: 2)
// Elliptical radius
(angle: 30deg, radius: (2, 1)) // (x-radius, y-radius)
Anchor Coordinates
// Reference named elements
circle((0, 0), name: "c")
line("c.east", (2, 0)) // from circle's east anchor
line("c.north", "c.south") // between anchors
Interpolation
// Point between two coordinates
(a, 50%, b) // midpoint
(a, 0.25, b) // 25% from a to b
((0,0), 1cm, (2,2)) // 1cm from first point toward second
Perpendicular Intersection
// Intersection of horizontal through a and vertical through b
(a, "|-", b)
(a, "-|", b) // opposite order
Projection
// Project point onto line
(pt, "_|_", line-start, line-end)
(project: pt, onto: (a, b))
Barycentric
// Weighted combination of vectors
(bary: (a: 1, b: 2, c: 1)) // weighted average
Tangent
// Point where line from external point touches shape tangentially
(element: "circle", point: "external-point", solution: 1) // solution: 1 or 2
Function Coordinate
// Apply function to resolved coordinate
(v => cetz.vector.add(v, (0, -1)), "element.anchor")
Drawing Functions API
line
line(..pts-style, close: false, name: none)
..pts-style: Two or more coordinates, plus optional style key-value pairsclose: Whentrue, closes the path to form a polygonname: Optional element identifier- Anchors: path anchors +
centroid(for closed non-self-intersecting polygons)
// Basic line
line((0, 0), (2, 1))
// Multiple points
line((0, 0), (1, 1), (2, 0), (3, 1))
// Closed path
line((0, 0), (1, 1), (1, 0), close: true)
// With arrow
line((0, 0), (2, 0), mark: (end: ">"))
line((0, 0), (2, 0), mark: (start: "<", end: ">"))
circle
circle(..points-style, name: none, anchor: none)
..points-style: Position coordinate; if two coords given, distance = radiusradius(style): number or(x-radius, y-radius)for ellipse (default: 1)- Anchors: border, path, center (default)
// Circle (default radius: 1)
circle((0, 0))
circle((0, 0), radius: 10pt)
// Ellipse
circle((0, 0), radius: (2, 1)) // (x-radius, y-radius)
// Circle through 3 points
circle-through((0, 0), (1, 1), (2, 0))
rect
rect(a, b, name: none, anchor: "center", ..style)
a: Bottom-left corner coordinateb: Top-right corner; use(rel: (width, height))for relative sizingradius(style): Corner rounding - number, ratio, or per-corner dict with keysnorth,east,south,west,north-west,north-east,south-west,south-east,rest- Anchors: border, path, center (default)
// Two corners
rect((0, 0), (2, 1))
// With radius (rounded corners)
rect((0, 0), (2, 1), radius: 0.2)
// Per-corner radius
rect((0, 0), (2, 1), radius: (north-west: 0.5, rest: 0.1))
arc
arc(position, start: auto, stop: auto, delta: auto, name: none, anchor: none, ..style)
- Must specify 2 of 3:
start,stop,delta radius(style): number or(x, y)for elliptical (default: 1)mode(style):"OPEN"(arc only),"CLOSE"(chord),"PIE"(sector)- Anchors:
arc-start,arc-end,arc-center,origin,chord-center, border, path
// Start and stop angles
arc((0, 0), start: 0deg, stop: 90deg, radius: 1)
// Delta angle
arc((0, 0), start: 0deg, delta: 270deg, radius: 1)
// Modes
arc((0, 0), start: 0deg, stop: 270deg, radius: 1, mode: "OPEN") // default
arc((0, 0), start: 0deg, stop: 270deg, radius: 1, mode: "PIE") // filled wedge
arc((0, 0), start: 0deg, stop: 270deg, radius: 1, mode: "CLOSE") // chord
// Arc through 3 points
arc-through((0, 0), (1, 1), (2, 0))
bezier
bezier(start, end, ..ctrl-style, name: none)
start,end: Start and end coordinates..ctrl-style: 1 control point = quadratic, 2 = cubic- Anchors: path anchors +
ctrl-0,ctrl-1(control points)
// Quadratic (1 control point)
bezier((0, 0), (2, 0), (1, 1))
// Cubic (2 control points)
bezier((0, 0), (3, 0), (1, 1), (2, 1))
// Bezier through 3 points (auto-calculates control points)
bezier-through((0, 0), (1, 1), (2, 0))
Grid
grid((0, 0), (4, 4))
grid((0, 0), (4, 4), step: 0.5)
grid((0, 0), (4, 4), step: (1, 0.5)) // different x/y steps
content
content(..args-style, angle: 0deg, anchor: "center", name: none)
- Single coord: place at position; two coords: place inside rectangle
angle: Rotation angle or coordinate to point towardpadding(style): Spacing around contentframe(style):"rect","circle", ornone- Anchors:
center,mid,mid-east,mid-west,base,base-east,base-west
content((1, 1), [Hello World])
content((1, 1), $x^2 + y^2 = r^2$)
content((1, 1), anchor: "north", [Label])
content((0, 0), (2, 1), [Fits in box]) // content inside rectangle
Other Shape Functions
// Regular polygon
polygon((0, 0), vertices: 6, radius: 1)
// Star shape
n-star((0, 0), n: 5, inner-radius: 0.5, outer-radius: 1)
// Smooth curves through points
hobby((0, 0), (1, 1), (2, 0)) // Hobby spline
catmull((0, 0), (1, 1), (2, 0)) // Catmull-Rom spline
// Merge multiple paths into one
merge-path({
line((0, 0), (1, 0))
arc((), start: 0deg, delta: 180deg, radius: 0.5)
line((), (0, 0))
})
Styling
Direct Styling
// Stroke
line((0, 0), (1, 1), stroke: red)
line((0, 0), (1, 1), stroke: blue + 2pt)
line((0, 0), (1, 1), stroke: (paint: green, thickness: 1.5pt, dash: "dashed"))
// Fill
circle((0, 0), fill: blue)
rect((0, 0), (1, 1), fill: red.lighten(50%))
// Combined
circle((0, 0), fill: yellow, stroke: orange + 2pt)
Global Styles
set-style(stroke: black + 0.5pt, fill: none)
// Element-specific defaults
set-style(
circle: (fill: blue.lighten(80%)),
line: (stroke: red),
)
Style Properties
fill: color ornonestroke: color, length, dictionary, ornonepaint: stroke colorthickness: line widthdash:"solid","dashed","dotted","dash-dotted"cap:"butt","round","square"join:"miter","round","bevel"
fill-rule:"non-zero"or"even-odd"(for self-intersecting paths)
Anchors
Named Anchors
Elements have type-specific anchors:
circle((0, 0), name: "c")
// Available: c.center, c.north, c.south, c.east, c.west, etc.
rect((0, 0), (2, 1), name: "r")
// Available: r.center, r.north, r.south, r.east, r.west,
// r.north-east, r.north-west, r.south-east, r.south-west
Border Anchors
Angles from center to border:
"element.0deg" // east (right)
"element.90deg" // north (up)
"element.45deg" // northeast
Path Anchors
Points along an element's path:
"element.start" // path start
"element.mid" // path midpoint
"element.end" // path end
"element.50%" // 50% along path
"element.2cm" // 2cm along path
Positioning with Anchors
// Place element's anchor at position
circle((0, 0), anchor: "west") // west anchor at origin
// Connect elements
circle((0, 0), name: "a")
circle((3, 0), name: "b")
line("a.east", "b.west")
Marks (Arrows)
Basic Marks
line((0, 0), (2, 0), mark: (end: ">"))
line((0, 0), (2, 0), mark: (start: "<", end: ">"))
line((0, 0), (2, 0), mark: (end: "stealth"))
Mark Symbols
">","<"- simple arrows"stealth"- stealth arrow"|"- bar"o"- circle"<>"- diamond"[","]"- brackets
Mark Options
line((0, 0), (2, 0), mark: (
symbol: ">", // mark symbol (or use start/end)
start: "<", // mark at path start
end: ">", // mark at path end
length: 0.2cm, // size in pointing direction
width: 0.15cm, // size perpendicular to direction
inset: 0.05cm, // distance inward for details
scale: 1, // multiplier for length/width/inset
sep: 0.1cm, // separation between multiple marks
pos: none, // absolute/relative position on path
offset: none, // advance position instead of override
anchor: "tip", // "tip", "center", or "base"
slant: 0%, // rotation relative to arrow axis
harpoon: false, // draw only top half
flip: false, // reverse orientation
reverse: false, // change direction
fill: auto, // fill color
stroke: auto, // stroke color
))
Transformations
// Scale
scale(2)
scale(x: 2, y: 0.5)
// Rotate
rotate(45deg)
rotate(45deg, origin: (1, 1))
// Translate
translate((2, 1))
// Group with local transform
group({
rotate(45deg)
rect((0, 0), (1, 1))
})
Grouping Functions
// Group elements with shared transform/style scope
group({
rotate(45deg)
rect((0, 0), (1, 1))
})
// Hide elements (still creates anchors)
hide({ line((0, 0), (2, 2), name: "helper") })
// Find intersections between named elements
intersections("i", "line1", "line2")
circle("i.0", radius: 2pt) // first intersection
// Define anchor in current group
anchor("my-anchor", (1, 1))
// Copy anchors from another element
copy-anchors("source-element")
// Iterate over element's anchors
for-each-anchor("element", (name, pos) => {
circle(pos, radius: 2pt)
})
// Assign to layer (for z-ordering)
on-layer("background", {
rect((0, 0), (5, 5), fill: gray)
})
// Float without affecting bounding box
floating({
content((10, 10), [Outside])
})
Tree Library
#import cetz.tree: tree
#cetz.canvas({
import cetz.draw: *
import cetz.tree: *
tree(
([Root], ([A], [A1], [A2]), ([B], [B1])),
direction: "down",
grow: 1.5,
spread: 2,
)
})
Tree Options
direction:"up","down","left","right"grow: distance between levelsspread: distance between siblingsdraw-node: custom node drawing callbackdraw-edge: custom edge drawing callback
Plot Library
#import "@preview/cetz:0.5.0"
#import "@preview/cetz-plot:0.1.3": plot, chart
#cetz.canvas({
import cetz.draw: *
plot.plot(
size: (8, 6),
x-tick-step: 1,
y-tick-step: 1,
{
plot.add(domain: (0, 4), x => calc.pow(x, 2))
plot.add(((0, 0), (1, 2), (2, 1), (3, 3)))
}
)
})
Common Patterns
Flowchart
#cetz.canvas({
import cetz.draw: *
rect((0, 0), (2, 1), name: "start", fill: green.lighten(80%))
content("start", [Start])
rect((0, -2), (2, -1), name: "process", fill: blue.lighten(80%))
content("process", [Process])
line("start.south", "process.north", mark: (end: ">"))
})
Coordinate Axes
#cetz.canvas({
import cetz.draw: *
line((-0.5, 0), (4, 0), mark: (end: ">"))
line((0, -0.5), (0, 3), mark: (end: ">"))
content((4, 0), anchor: "west", $x$)
content((0, 3), anchor: "south", $y$)
})
SVG Paths (0.5.0+)
// Render SVG path data directly
svg-path("M 0 0 L 2 0 L 1 1 Z")
Perspective Projection (0.5.0+)
// Native perspective projection mode
#cetz.canvas({
import cetz.draw: *
// Apply perspective transform
set-transform(cetz.matrix.transform-perspective(distance: 10))
// 3D drawing with perspective
line((0, 0, 0), (2, 0, 0))
line((0, 0, 0), (0, 2, 0))
line((0, 0, 0), (0, 0, 2))
})
Anti-Patterns
| Avoid | Prefer | Reason |
|---|---|---|
| Hard-coded positions | Named elements + anchors | Maintainable |
| Repeated styling | set-style() |
DRY |
| Complex inline calculations | Interpolation coordinates | Readable |
| Manual arrow drawing | mark parameter |
Consistent |
Touying Integration
For animated diagrams in slide presentations, use CeTZ with Touying's touying-reducer. See /typst-touying skill for full documentation.
#let cetz-canvas = touying-reducer.with(
reduce: cetz.canvas,
cover: cetz.draw.hide.with(bounds: true)
)
#slide[
#cetz-canvas({
import cetz.draw: *
rect((0, 0), (2, 2))
(pause,) // Animation marker
circle((3, 1), radius: 0.5)
})
]