Zap

Your Typst circuit drawing companion

Version 0.5.0 • Edit this file

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:

  1. 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.
  2. 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.

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

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 at inX 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:

Flipflop

#zap.circuit({
import zap: *

flipflop("f1", (0, 0))
dflipflop("f2", (3, 0))
jkflipflop("f3", (6, 0))
})
A project initiated by Louis Grange, under LGPL-3.0 license