Introduction
About the project
I initiated Zap in 2025 during my microengineering studies at EPFL (Federal Institute of Technology, Lausanne) in Switzerland 🇨🇭. I aim to leverage my engineering background to create a tool that is both powerful and a pleasure to use.
Philosophy
Instead of simply porting the popular circuitikz library to Typst, this project tries to establish a new foundation. Zap is built around two core principles:
- Keep functions clean and intuitive. This ensures the library is easy to use, requiring minimal code while remaining highly customizable. Users can take advantage of Typst’s modernity, avoiding the need to input overly complex symbols or lengthy commands.
-
Always rely on standard-inspired symbols when possible. This is the most significant departure from
circuitikz, which prioritized extensive customization. With Zap, you get direct access to symbols inspired by international standards used in industry.Purely aesthetic symbol variations are therefore excluded, as this project is focusing on making the standard symbols visually excellent. This ensures your circuits will be understandable by anyone, anywhere. Of course, I know this might be a constraint, so Zap will always give you the freedom to create your own custom symbols.
Contributors
Special thanks to all the contributors who bring amazing features and bug fixes.
How to contribute?
Contributions are very welcome, and it’s very easy to set up. You can find more information on how to start on the dedicated contributing guidelines.Getting started
After this quick introduction, let’s get started! You can start using Zap simply by adding the following import at the top of your Typst file. It will automatically install the library from Typst Universe.
#import "@preview/zap:0.5.0"
#zap.circuit({
import zap: *
isource("i1", (0, 0), (5, 0))
})
Positioning
The most important part of this library is to know how to position your symbols within your circuit, as it can make your experience a lot more pleasant.
You can choose to either attach your symbols to a single node, or place them between two nodes. If you choose the latest option, wires will be automatically placed between the nodes, like below.
#zap.circuit({
import zap: *
// using one node ...
resistor("r1", (-2, 0))
// ... or using two nodes
resistor("r2", (1, 0), (4, 0))
})
Warning
Note that some symbols can only be placed using one node, like operational amplifiers, grounds and transistors.You can also customize the position of the symbol alongside the wire using the position parameter like below.
#zap.circuit({
import zap: *
resistor("r1", (0, 0), (3, 0), position: 70%)
})
The position parameter also accepts a distance, which is always relative to the in anchor.
Mirroring or flipping
If you would like to display your component upside-down (vertically and/or horizontally), it is possible to mirror it using the scale parameter.
#zap.circuit({
import zap: *
nobutton("b1", (0, 0), (3, 0))
nobutton("b2", (3, 0), (6, 0), scale: (y: -1))
})
Named anchors
Sometimes, you just want to connect one symbol to another without worrying about coordinates or doing mental math. That’s where named anchors come in.
The name provided as the first argument acts as an identifier for the symbol. If we draw a resistor identified as r1, we can attach a voltage source to its out anchor like below.
#zap.circuit({
import zap: *
resistor("r1", (0, 0), (3, 0))
vsource("v1", "r1.out", (3, 3))
})
A list of available named anchors is available on each symbols. You can also activate debugging to display the available anchors directly on your circuit.
Nodes
You can also use the node symbol instead of anchors. They work pretty much the same, but nodes are visible on the circuit.
#zap.circuit({
import zap: *
node("n1", (0, 0), label: "MyNode")
})
It’s also possible to display nodes directly when calling your symbol, and they will represent the in and out anchors.
#zap.circuit({
import zap: *
resistor("r1", (1, 0), (4, 0), n: "*-*")
})
You can either have circles using o or a filled one using *. For example, o-* will have a circle on the in anchor and a filled circle on the out anchor.
Coordinates
Coordinates are fully managed by CeTZ, and you’ll find a very extensive list of features on their online documentation, like relative, perpendicular and polar coordinates. Let’s just have a quick look at the main features you’ll use in your circuits.
Perpendicular
You can easily define a new coordinate with the perpendicular position between 2 other coordinates.
#zap.circuit({
import zap: *
node("n0", (0, 0), label: "O")
node("n1", (3, -1), label: "A")
// using either (A, "-|", B) or (A, "|-", B) is possible
node("n2", ((0, 0), "-|", "n1"), label: "P")
})
Relative coordinates
You can also define the new coordinate using a previously defined anchor.
In the example below, we want to point r2 to the out anchor of r1, but a little bit on the right.
#zap.circuit({
import zap: *
// relative to the previous coordinate, here (1, 0)
resistor("r1", (1, 0), (rel: (3, 0)))
// relative to a specific coordinate
resistor("r2", (5, -3), (rel: (1, 0), to: "r1.out"))
})
Labels
You can name your symbols by giving them a label using the label parameter.
#zap.circuit({
import zap: *
heater("h1", (1, 0), (4, 0), label: $R$)
})
Sometimes, the label is not displayed where you want (like in the middle of another symbol). In that case, you can just give a dictionary to customize this behavior.
#zap.circuit({
import zap: *
heater("h1", (1, 0), (4, 0), label: $R$)
heater("h2", (5, 0), (8, 0), label: (content: $R$, anchor: "south", distance: 0pt))
})
Decorations
You can add labels for current, voltage, or generic flow to your symbols using the i (current), u (voltage), or f (flow) parameters, which accept either a string for a simple label or a dictionary for more detailed customization.
#zap.circuit({
import zap: *
vsource("v1", (1, 0), (5, 0), u: $u_1$, i: $i_1$)
})
Current
#zap.circuit({
import zap: *
// simple current
vsource("v1", (1, 0), (4, 0), i: $i_1$)
// custom current (only "content" key is required)
vsource("v1", (5, 0), (8, 0), i: (content: $i_1$, anchor: "west", invert: true, distance: 17pt, label-distance: 15pt))
})
Voltage
#zap.circuit({
import zap: *
// simple voltage
vsource("v1", (1, 0), (4, 0), u: $u_1$)
// custom voltage (only "content" key is required)
vsource("v1", (5, 0), (8, 0), u: (content: $u_1$, anchor: "south-west", label-distance: 8pt, distance: 17pt))
})
Flow
#zap.circuit({
import zap: *
// simple flow
vsource("v1", (1, 0), (4, 0), f: $f_1$)
// custom flow (only "content" key is required)
vsource("v1", (5, 0), (8, 0), f: (content: $f_1$, anchor: "south-west", label-distance: -20pt, distance: 17pt))
})
Annotations
As the circuit is just a boosted version of CeTZ’ canvas, you can also directly draw shapes on it.
#zap.circuit({
import zap: *
// classic circuit
vsource("v1", (1, 0), (1, 3), u: $u_1$)
resistor("r1", "v1.out", (rel: (3, 0)), i: $i_1$)
// annotations
draw.rect((0.7, 2.5), (4, 4), stroke: (dash: "dashed", thickness: .8pt, paint: red), name: "rect")
draw.content("rect.north", text(fill: red)[This is a rectangle], anchor: "south")
draw.line((2, 2), (4, 0), mark: (end: ">", fill: purple), stroke: purple + .8pt)
})
Stubs
Sometimes, you’ll just want to add a small wire with a label to show an entry point. Stubs do just that, in any vertical or horizontal direction you want.
#zap.circuit({
import zap: *
mcu("m1", (0, 0), pins: 10, label: "ESP32", fill: green.lighten(80%), stroke: none)
wstub("m1.pin2", label: "PORTB2") // or stub(..., dir: "west")
estub("m1.pin6", label: "PORTC2")
estub("m1.pin9", label: "PORTD5", length: 0.5)
})
To simplify your code, you can use nstub, sstub, estub, and wstub for quick directions.
Styling
If you want to customize the appearance of a single symbol instance, rather than all, simply use the various params optional arguments.
#zap.circuit({
import zap: *
resistor("r1", (0, 0), (3, 0))
resistor("r2", (3, 0), (6, 0), variant: "ieee", stroke: 1pt + red)
})
Info
As the list of available styles for each component is too long, it is only available in the source code.Global
If you wish to change the default appearance of all symbols of a specific type throughout the same circuit, Zap supports the set-style method from CeTZ.
#zap.circuit({
import zap: *
set-style(resistor: (stroke: red, scale: (x: 0.5, y: 1.5)))
resistor("r1", (0, 0), (3, 0))
heater("r2", (3, 0), (6, 0))
})
Standards
As industries have become more interconnected through globalization, numerous standards have emerged. Many of these have since been replaced by a smaller set of conventions that have gained wider global acceptance.
- IEC 60617 (default) — The leading international standard, widely adopted.
- IEEE/ANSI 315-1975 — The North-American standard for electrical and electronics symbols.
- JIS C 0617 — The official Japanese standard, largely harmonized with the international IEC series.
- GB/T 4728 — The official Chinese national standard, also closely aligned with IEC guidelines.
Zap currently supports either iec (default) or ieee in the variant styling parameter.
#zap.circuit({
import zap: *
// remember you can use global styling
// set-style(variant: "ieee")
resistor("r1", (0, 0), (3, 0))
resistor("r2", (3, 0), (6, 0), variant: "ieee")
})
Wiring
You can choose between squared, zigzag or straight wires using swire, zwire or wire.
#zap.circuit({
import zap: *
wire((0, 0), (1, 0))
zwire((2, 0), (4, 2), stroke: blue)
swire((5, 2), (6, -1), stroke: red)
})
Customization
The position and axis of the wire can also be altered using the axis and ratio parameters.
#zap.circuit({
import zap: *
wire((0, 0), (1, 0)) // N/A
zwire((2, 0), (4, 2), stroke: blue, axis: "y", ratio: 80%)
swire((5, 2), (6, -1), stroke: red, axis: "y")
})
Anchors
in: the first pointout: the last pointp<i>: an anchor for each given point of the wire (by index i)p<i>-p<i+1>.a: the first zigzag corner between the points i and i+1 (only for zigzag)p<i>-p<i+1>.b: the second zigzag corner between the points i and i+1 (only for zigzag)
Debug
If you want to know all the anchors available in a symbol, you can either activate the debug mode on a single symbol…
#zap.circuit({
import zap: *
resistor("r1", (0, 0), (3, 0), debug: true)
})
…or on the whole circuit.
#zap.circuit(debug: true, {
import zap: *
resistor("r1", (0, 0), (3, 0))
})
Available symbols
As there is a lot of symbols available in Zap, they have been grouped by their original version. If you’re not finding yours here, and think it should be included in the library, please open an issue.
Resistor
#zap.circuit({
import zap: *
resistor("r1", (0, 0), (3, 0))
resistor("r2", (4, 0), (7, 0), variant: "ieee")
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
heatable |
false |
heater |
|
adjustable |
false |
potentiometer |
|
variable |
false |
rheostat |
|
sensor |
false |
||
preset |
false |
Inductor
#zap.circuit({
import zap: *
inductor("i1", (0, 0), (3, 0))
inductor("i2", (4, 0), (7, 0), variant: "ieee")
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
variable |
false |
||
preset |
false |
||
sensor |
false |
Capacitor
#zap.circuit({
import zap: *
capacitor("c1", (0, 0), (3, 0))
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
variable |
false |
||
preset |
false |
||
sensor |
false |
||
polarized |
false |
pcapacitor |
Button
#zap.circuit({
import zap: *
button("b1", (0, 0), (3, 0))
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
nc |
false |
ncbutton |
|
illuminated |
false |
noibutton |
|
head |
"standard" |
||
latching |
false |
Voltage source
#zap.circuit({
import zap: *
vsource("b1", (0, 0), (3, 0))
vsource("b2", (4, 0), (7, 0), variant: "ieee")
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
dependent |
false |
dvsource |
|
current |
"dc" |
acvsource |
Current source
#zap.circuit({
import zap: *
isource("b1", (0, 0), (3, 0))
isource("b2", (4, 0), (7, 0), variant: "ieee")
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
dependent |
false |
disource |
Diode
#zap.circuit({
import zap: *
diode("b1", (0, 0), (3, 0))
})
Types
The diode symbol accepts only one parameter, called type, and its appearance changes a lot depending on the value.
| Value | Alias | Image |
|---|---|---|
zener |
zener |
|
tunnel |
tunnel |
|
schottky |
schottky |
|
emitting |
led |
|
recieving |
photodiode |
Supply
#zap.circuit({
import zap: *
wire((0, 0), (8, 0))
vcc("s1", (0, 0))
vee("s2", (2, 0))
earth("s3", (4, 0))
frame("s4", (6, 0))
ground("s5", (8, 0))
})
Fuse
#zap.circuit({
import zap: *
fuse("f1", (0, 0), (3, 0))
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
asymmetric |
false |
afuse |
Operational amplifier
#zap.circuit({
import zap: *
opamp("o1", (0, 0))
opamp("o2", (3, 0), variant: "ieee")
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
invert |
false |
iopamp |
Converter
#zap.circuit({
import zap: *
adc("c1", (0, 0), (3, 0))
dac("c2", (4, 0), (7, 0))
})
BJT transistors
#zap.circuit({
import zap: *
bjt("t1", (0, 0))
})
Options
| Name | Default value | Type | Alias | Image |
|---|---|---|---|---|
polarisation |
"npn" |
"npn" / "pnp" |
npn / pnp |
|
envelope |
false |
bool |
MOSFET transistors
#zap.circuit({
import zap: *
mosfet("t1", (0, 0))
})
Options
| Name | Default value | Type | Alias | Image |
|---|---|---|---|---|
channel |
"n" |
"n" / "p" |
pmos / nmos |
|
envelope |
false |
bool |
||
mode |
"enhancement" |
"enhancement" / "depletion" |
nmosd / pmosd |
|
bulk |
"internal" |
"internal" / "external" / none |
Transformer
#zap.circuit({
import zap: *
transformer("t1", (0, 0), (3, 0))
})
Instruments
#zap.circuit({
import zap: *
voltmeter("i1", (0, 0), (3, 0))
ammeter("i2", (4, 0), (7, 0))
ohmmeter("i3", (8, 0), (11, 0))
wattmeter("i4", (12, 0), (15, 0))
})
Switch
#zap.circuit({
import zap: *
switch("s1", (0, 0), (3, 0))
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
closed |
false |
Antenna
#zap.circuit({
import zap: *
wire((0, 0), (2, 0))
antenna("a1", (1, 0))
})
Circulator
#zap.circuit({
import zap: *
circulator("c1", (0, 0), (3, 0))
})
Logic
#zap.circuit({
import zap: *
lnot("l1", (0, 0))
land("l2", (2, 0))
lor("l3", (4, 0))
lxor("l4", (6, 0))
})
Options
| Name | Default value | Alias | Image |
|---|---|---|---|
invert |
false |
lnand / lnor / lxnor |
|
inputs |
2 |
Inputs
When setting a number of inputs, each input will be available atinX anchor. For example, for three inputs you will have access to l1.in1, l1.in2 and l1.in3.Microcontrolling unit
#let pins = (
(content: "VCC", side: "west"),
(content: "UVCC", side: "west"),
(content: "AVCC", side: "west"),
(side: "west"),
(content: "PD0", side: "west"),
(content: "PD1", side: "west"),
// ...
)
#zap.circuit({
import zap: *
mcu("mcu", (3, 0), pins: pins)
})
You have to provide either a number of pins or a complete list of dictionaries. Each pin can have these keys:
contentis the label of the pin displayed on the controller. If not provided, the pin is considered as a gap instead of an actual pin.siderepresents the position of the pin on the microcontroller. It is only possible to representwestandeastlabels for now, but support for more positions is planned.
Flipflop
#zap.circuit({
import zap: *
flipflop("f1", (0, 0))
dflipflop("f2", (3, 0))
jkflipflop("f3", (6, 0))
})