name: typst-cetz description: | CeTZ (TikZ-inspired drawing for Typst) patterns for creating diagrams. Use this skill when creating diagrams, flowcharts, graphs, plots, or technical illustrations in Typst documents. Covers coordinate systems, drawing primitives, styling, anchors, marks, trees, and plotting. license: CC-BY-4.0 metadata: author: Ulrich Atz
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)
})
]