Typst Package User Manual MIT License GitHub Issues Last Commit

Last updated: April 2026

1 Introduction

This manual introduces synkit and its functions. synkit is a Typst package designed to streamline the creation of syntactic structures while maintaining typographical precision. Typst is a programming language for typesetting. There is a good tutorial here, and an introductory YouTube series here.

The package provides functions for phrase-structure trees, movement arrows, multidominance, semantic annotation, cross-tree equivalence lines, in-line movement notation, numbered linguistic examples, and interlinear glosses. The main goals of synkit are to minimize effort and maximize quality: the syntax should stay intuitive and compact without sacrificing typographic quality.

There are at least two excellent packages in \(\LaTeX\) for syntax trees, tikz-qtree (Chiang 2009) and forest (Živanović 2017). Typst also has tree packages already, but synkit aims to cover a broader set of use cases that matter in syntax and semantics, especially for authors who want to migrate away from \(\LaTeX\) without giving up movement arrows, semantic composition, or richer tree layouts.

The GitHub repository for the package can be found at guilhermegarcia/synkit. Comments, suggestions, and bug reports are welcome.

1.1 Installation

Typst packages are loaded with #import at the top of your typ document. Replace X.X.X with the version you wish to import:

Package import (Typst Universe)

#import "@preview/synkit:X.X.X": *

Alternatively, if you want the most up-to-date version, clone the repository and load the package locally:

Local import

#import "synkit/lib.typ": *

You may need a symlink depending on how you structure your files, since Typst restricts imports to files within the compilation root and its subdirectories.

2 Basic trees

The main function for trees is #tree(), whose main argument is a bracketed string. Spaces are intentionally flexible, so [S[NP][VP]] and [ S [ NP ] [ VP ] ] are interpreted identically.

Figure 1: A basic syntax tree with terminal branches.

Basic tree

#tree(
  "[S [NP [Det the] [N cat]] [VP[V sat][PP[P on] [NP [Det the] [N mat]]]]]",
  terminal-branch: true,
)

By default, terminal branches are omitted, but you can change that by setting terminal-branch: true — this is shown in Figure 1 above. Triangles are added automatically when a phrase node directly dominates leaf content, as in [NP the cat]. If needed, you can also specify the triangle argument manually.

3 Arrows

Movement arrows are added with the arrows argument. Arrows can be rectangular or curved, and each arrow can be customized independently through keys such as dash, color, line-width, bend, and shift.

Figure 2: Multiple movement arrows using only the function’s defaults; adapted from Carnie.

Tree with arrows

#tree(
  "[ CP [ C' [ C Ø_{[+Q]}+T+Mangez ] [ TP [ DP vous ] [ T' [ T *t*_i ] [ VP [ *t*_DP ] [ V' [V *t*_i ] [DP des pommes] ] ]  ] ] ] ]",
  arrows: (
    (from: "trace3", to: "T1"),
    (from: "trace2", to: "DP1"),
    (from: "trace1", to: "C1"),
  ),
  curved: true,
)

Labels are created automatically, and arrow targets can be refined further with suffixes such as -up, -down, and optional degree values. This allows fine-grained control while keeping the common case minimal.

4 Semantics

The dominance argument adds long-distance dominance lines between nodes. This is useful for multidominance and other structures that cannot be represented with ordinary local branching alone. The annotation argument allows for comprehensive annotation in the tree. The example below comes from Fox and Johnson (2016).

Figure 3: Semantic annotation combined with multidominance.

Multidominance

#tree(
 "[IP [IP [IP [IP [DP† [D the_2] [\\muP every woman] ] 
 [IP [I] [VP is smiling] ] ] [IP [and] [IP [DP‡ [D the_2] [\\muP every man] ] 
 [IP [I] [VP is frowning] ] ] ] ] [\\lambda2] ] [QP [Q \\forall ] 
 [\\muP\\* [\\muP] [CP who came in together] ] ] ]",
 annotation: (
   (
     "IP1",
     [$forall$_y_ [_y_ is a woman+man $and$ _y_ came in together] $arrow$ \
       [the woman part of _y_ is smiling and the man part of _y_ is frowning]],
   ),
   (
     "IP2",
     [$lambda$_x_ : _x_ has a has a unique maximal woman part \
       and a unique maximal man part. \
       the woman part of x is smiling and \
       the man part of x is frowning],
   ),
   (
     "QP1",
     [$lambda$_Q_$forall$_y_[_y_ is woman+man \
       $and$ _y_ came in together] $arrow$ _Q(y)_],
   ),
   (
     "IP3",
     [the woman part of g(2) is smiling \
       and the man part of g(2) is frowning],
   ),
   (
     "DP†1",
     [the woman part \
       of g(2)],
   ),
   (
     "DP‡1",
     [the man part \
       of g(2)],
   ),
 ),
 annotation-size: 0.8,
 dominance: (
   (from: "muP4", to: "muP1", ctrl: (-6.1, 8.5)),
   (from: "muP4", to: "muP2", ctrl: (-6, 5)),
 ),
 scale: 0.8,
 spread: 0.8,
 terminal-branch: true,
)

The same tree can still use local spacing arguments such as spread-local and drop-local, which means multidominant trees remain controllable without editing absolute coordinates.

5 Bilingual trees

The #garden() function allows multiple trees to share a single canvas with dashed equivalence lines between corresponding nodes. This is useful for bilingual comparisons or any scenario where two structures need to be aligned visually. The example below comes from David Chiang’s tutorial on tikz-qtree.

Figure 4: Bilingual trees with cross-tree equivalence lines.

Two trees with equivalence lines

#garden(
  (
    input: "[S [NP [Det the] [N cat]] [VP [V sat] [PP [P on] [NP [Det the] [N mat]]]]]",
    spread: 1.55,
    content-size: 1,
  ),
  (
    input: "[S [NP 猫が] [VP [PP [NP [NP マット] [Part の] [NP 上] ] [P に]] [V 土]]]",
    direction: "up",
    content-size: 1,
  ),
  equivalence: (
    ("Det1-1", "NP1-2"),
    ("P1-1", "P1-2"),
    ("P1-1", "NP4-2"),
    ("N1-1", "NP1-2"),
    ("N2-1", "NP3-2"),
    ("Det2-1", "NP3-2"),
    ("V1-1", "V1-2"),
  ),
  gap: 2.5,
  scale: 0.7,
)

6 In-line movement and numbered examples

In-line movement notation is handled by #move(). Each word automatically becomes a labelable anchor, so arrows can target visible copies, traces, or words without manual anchor definitions.

Figure 5: In-line movement inside a numbered example.

In-line movement

#show: eg-rules

#eg(labels: (<s-plain>, <s-move>))[
  - Who do you think saw Mary?
  - #move(
      "[CP Who do you think [(CP)[TP<who>saw Mary]]]",
      arrows: ((from: "who2", to: "who1", dash: "solid", color: black),),
    )
] <eg-wh>

When used as a standalone object, protect: true is useful because it reserves vertical space for the arrows below the text. Inside numbered examples or table cells, protect: false is often the better default because it keeps baseline alignment cleaner.

The labels argument allows individual subexamples to be referenced. The title argument places a title on the same line as the example number, and caption becomes useful if you later generate an outline targeting only linguistic examples.

7 Glosses

The #gloss() function creates aligned interlinear glosses with the same numbering stream as #eg(). Its syntax is intentionally minimal: each line is simply a list item, and the function aligns material by splitting on spaces. The example below comes from O’Grady and Archibald (2016).

Figure 6: A four-line Inuktitut gloss.

Gloss with four lines and escaped orthographic form

#show: eg-rules

#gloss(per: 4, escape: (0,), caption: [An example from Inuktitut])[
  - Qasuiirsarvigssarsingitluinarnarpuq
  - Qasu -iir -sar -vig -ssar -si -ngit-luinar -nar -puq
  - tired not cause-to-be place-for suitable find not-completely someone 3.{sg}
  - 'Someone did not find a completely suitable resting place.'
]

The per argument controls how many lines belong to each gloss, while escape lets you exempt specific lines from the parsing/alignment logic.

8 Extras

This section gathers a number of arguments that make trees easier to tune in practice.

8.1 Spacing

Four arguments are central to spacing in #tree():

Parameter Purpose
spread Global spacing between sister nodes
spread-local Local spacing between sister nodes
drop Global spacing between levels
drop-local Local spacing between levels

All four operate on direct multipliers, which makes them easier to reason about than absolute positional tweaks.

Spacing example

#tree(
  "[ A [ B [C] [D] ] [ E [F] [G] ] ]",
  spread-local: ( ("C1", 0.4), ),
  drop-local: (
    ("C1", 0.5),
    ("F1", 1.3),
  ),
  arrows: ("C1", "F1"),
)

8.2 Line breaks

Long phrases can be broken with \\n, which is often preferable to forcing a tree to become too wide.

Line breaks inside phrases

#tree(
  "[S [NP the orange cat \\n that lives next door]
    [VP[V sat][PP[P on]
    [NP the new couch \\n we purchased \\n last week]]]]",
)

8.3 Formatting

Because tree input is a string, formatting shortcuts matter. #tree() supports italics, bold, small caps, superscripts, subscripts, and symbol shortcuts directly in the input string. Highlighting is handled separately through the highlight argument.

Figure 7: Color, font, emoji, and branch styling in a single tree.

Formatting shortcuts

#tree(
  "[S [NP the 🐈] [VP[V sat][PP[P on] [NP the mat]]]]",
  content-size: 1,
  drop: 0.8,
  spread: 1.5,
  dash-branches: (
    ("VP1", "V1"),
    ("S1", "VP1"),
  ),
  color: (
    ("S1", green.darken(20%)),
    ("NP1", red),
    ("NP2", red),
    ("P1down", blue),
    ("VP1", "V1", orange),
  ),
  font: "Comic Sans MS",
)

8.4 Direction

Trees can grow in four directions: "down", "up", "left", and "right". This is particularly important for mirrored bilingual trees and compact sideward layouts.

Changing tree direction

#tree(
  "[S [NP_\\omega the *cat*] [VP[V @sat@][PP[P on] [NP^\\phi the **mat**]]]]",
  direction: "left",
)

8.5 Sizing

The arguments scale, content-size, and node-size affect different layers of the layout. scale changes the whole object, while content-size and node-size tune terminal content and node labels more locally.

Sizing example

#tree(
  "[S [NP the cat] [VP[V sat][PP[P on] [NP the mat]]]]",
  content-size: 1.2,
  node-size: 0.6,
  drop: 0.8,
  spread: 1.5,
)

8.6 Branch styling

Branch appearance can be changed with dash-branches, while color can target nodes, leaf content, or specific parent-child branches.

8.7 Font

Each function inherits the document font by default, but you can choose a local font with the font argument. This can be useful when you want a single tree to stand out or when a particular font handles symbols more gracefully.

9 Future work and acknowledgments

The package is still young, so there will be bugs and limitations. Comments and suggestions are especially helpful at this stage because they directly improve precision and coverage. The package has also benefited from feedback by colleagues in syntax and semantics.

10 Argument reference

The table below summarizes the arguments currently available in #tree().

Argument Type Default Description
arrows array () Cross-node arrows as tuples or dicts
scale number 1.0 Uniform scale factor for the whole tree
triangle array () Anchor names rendered with a triangle
content-size number 0.8 Size multiplier for leaf content
node-size number 1.0 Size multiplier for node labels
curved bool false Use Bézier curves instead of rectangular arrows
direction string "down" Growth direction
highlight array () Anchors to box
bottom bool false Align all leaves at the lowest level
terminal-branch bool false Draw branches to terminals
dash-branches array () Dashed parent-child branches
delinks array () Delink marks on arrow shafts
index array () Coreference subscripts
append array () Extra subscript text after a node label
drop number 1.0 Vertical spacing multiplier
drop-local array () Per-level or per-node vertical adjustments
spread number 1.0 Horizontal width per leaf
spread-local array () Per-level or per-node horizontal adjustments
dominance array () Long-distance dominance lines
color array () Colors for nodes, leaves, or branches
annotation array () Content between a node label and its branches
annotation-size number 0.70 Size multiplier for annotation text
annotation-leading length or auto auto Line spacing for multi-line annotations
numbers array () Circled numbers next to node labels
numbers-size number 0.85 Size multiplier for circled numbers
line-width number 1.0 Stroke width multiplier for tree lines
font string none Local font family

11 How can I use Typst offline?

While the online editor at Typst.app is very convenient, many users prefer to work locally. One of the best IDE options is VS Code with the Tinymist extension (Dreamin and Varner 2024). Tinymist is also available in other editors, including NeoVim and Positron.

12 How do packages work in Typst?

Typst packages are imported rather than installed system-wide in the way LaTeX or Python users may expect. The two common workflows are:

  1. Import from the official Typst package repository with @preview/....
  2. Clone a repository locally and import its lib.typ file.

If the package is stored outside your document root, a symlink is often the simplest solution.

Creating a local symlink to a package

# From your working directory:
ln -s PATH_TO_PACKAGE_DIRECTORY package_name

Typst’s package repository also keeps each released version in its own directory, which is why explicit version imports are so important.

Copyright © Guilherme Duarte Garcia

References

Chiang, David. 2009. Tikz-Qtree – Use Existing Qtree Syntax for Trees in TikZ. https://ctan.org/pkg/tikz-qtree.
Dreamin, Myriad, and Nathan Varner. 2024. Tinymist: An Integrated Language Service for Typst. Version 0.14.4. https://github.com/Myriad-Dreamin/tinymist.
Fox, Danny, and Kyle Johnson. 2016. “QR Is Restrictor Sharing.” In Proceedings of the 33rd West Coast Conference on Formal Linguistics, edited by Kyeong-min Kim, Pocholo Umbal, Trevor Block, et al. Cascadilla Proceedings Project.
O’Grady, William, and John Archibald. 2016. Contemporary Linguistic Analysis: An Introduction. 8th ed. Toronto: Pearson.
Živanović, Sašo. 2017. The Forest Package: Drawing Linguistic (and Other) Trees. https://ctan.org/pkg/forest.