Forms for Typst templates
Give your own Typst functions a form, so people who fill in a report see labelled fields in Visual mode instead of function-call syntax.
A Typst template often ships its own functions: a cover page, an info box, a sign-off block. Whoever writes the report calls them, as in #info-box(title: "Supply risk", tone: "risk")[...]. If you maintain the template, you can describe each function’s arguments once. Visual mode then shows every call to that function as a form: labelled fields, a list for fixed choices, a switch for yes or no. Editing a field rewrites only that one argument in the .typ file, so the report stays ordinary Typst source that compiles the same way.
What the reader sees
Section titled “What the reader sees”A call on its own lines becomes a card. The card’s header shows the form’s name and, if you choose one, a short summary such as the box’s title. Each argument gets a row with its label and a control.
A call inside a sentence, such as #badge("review"), becomes a chip in the line of text. Clicking the chip opens the same fields in a small popover.
Text fields and markup fields save when you press Enter (⌘ Enter or Ctrl Enter in a multi-line field) or leave the field. Esc puts the old value back. Lists and switches save at once. Every change is one undo step.
Moving the cursor into a call shows its source, like any other Visual mode element. Edit in source on the card does the same.
Where to declare forms
Section titled “Where to declare forms”There are two places, with the same format:
- In a package. Add a
[tool.oleafly]section to the package’stypst.toml. Typst keeps[tool.*]sections for other tools, so the package still installs and compiles everywhere. This works for@localpackages and for@previewpackages from Typst Universe. - In the project. For templates kept as plain
.typfiles inside the project, createoleafly-forms.tomlat the project root. Each form also names the file that defines the function, withmodule.
A package form:
[tool.oleafly]schema = 1
[[tool.oleafly.forms]]function = "info-box"label = "Info box"summary = "title"
[[tool.oleafly.forms.fields]] arg = "title" kind = "text" label = "Heading" placeholder = "Short heading" required = true
[[tool.oleafly.forms.fields]] arg = "tone" kind = "choice" label = "Tone" default = "note" options = [{ value = "note", label = "Note" }, { value = "risk", label = "Risk" }, "neutral"]
[[tool.oleafly.forms.fields]] arg = 0 kind = "content" label = "Text"A project form, in oleafly-forms.toml:
[tool.oleafly]schema = 1
[[tool.oleafly.forms]]module = "template/memo.typ"function = "memo-header"label = "Memo header"fields = [ { arg = "to", kind = "text", label = "To" }, { arg = "subject", kind = "text", label = "Subject" },]The keys
Section titled “The keys”schema = 1 is required. A file without it, or with another number, is ignored.
Each [[tool.oleafly.forms]] entry has:
| Key | Required | Meaning |
|---|---|---|
function | Yes | The function’s name, as one Typst identifier. Hyphens are fine. |
module | In oleafly-forms.toml only | The project file that defines the function, relative to the project root, with / between folders. |
label | No | The form’s name on cards and chips. Defaults to the function’s name. |
summary | No | The argument whose value appears next to the label, written like a field’s arg. |
fields | Yes | One or more fields. |
Each field has:
| Key | Required | Meaning |
|---|---|---|
arg | Yes | A name for a named argument ("title"), or a number for a positional one (0 is the first). A trailing [...] block counts as a positional argument after the ones in parentheses. |
kind | Yes | text, content, choice, number or toggle. |
label | Yes | The field’s label. |
help | No | A short note under the field. |
placeholder | No | Grey text in an empty field. |
required | No | true marks an empty field as missing and hides its Clear button. Oleafly never adds the argument by itself. |
default | No | The value your function uses when the argument is left out. It is shown, never written. |
Kinds and their extra keys:
textwrites a Typst string. Addmultiline = truefor a larger box.contentedits Typst markup and writes it as a[...]block. A string argument stays a string.choiceneedsoptions: a list of strings, or{ value, label }tables when the label differs from the value. Values are written as strings.numbertakesinteger,min,maxandstep. Numbers have no units.togglewritestrueorfalse.
Which calls get a form
Section titled “Which calls get a form”Oleafly follows the report’s own #import and #let lines at the top level of the file:
#import "@local/acme-report:0.1.0" as acmemakes#acme.info-box(...)a form.#import "@local/acme-report:0.1.0": info-box, status-badge as badgemakes#info-box(...)and#badge(...)forms.#import "@local/acme-report:0.1.0": *makes every function the package declares a form for.#import "template/memo.typ": memo-headermatches a project form whosemoduleistemplate/memo.typ. Paths are read from the report’s own folder, or from the project root when they start with/.
The package version must match exactly. A #let with the same name, later in the file, replaces the form from that point on. Calls reached through another file that re-imports the package keep their source text.
A function named like a Typst element that Visual mode already draws, such as table or figure, keeps the built-in look when called by its bare name. #acme.table(...) or a renamed import still gets the form.
Fields shown read-only
Section titled “Fields shown read-only”Some arguments can’t be edited safely from a form. Their row shows the source with the reason, and Edit in source jumps to it:
- Computed in the source: the value is an expression, such as
title: data.title. - Written as a different kind of value: for example a number where the form expects text.
- Not one of the choices: a value your
optionsdon’t list. - Set by a spread argument: a call with
..argspasses values the form can’t see, so every field is read-only.
When a field can’t be added where it would go, for example a positional argument after a missing one, the form says so and offers Add in source.
Mistakes in a definition
Section titled “Mistakes in a definition”Open oleafly-forms.toml, or a vendored package’s typst.toml, in Oleafly and each problem shows as a marker on its line: an unknown kind, a missing label, a duplicate form, a module outside the project. A broken form is left out and the others keep working. A file larger than 256 KB is not read.
Sharing templates
Section titled “Sharing templates”Oleafly reads package forms from the same folders the compiler uses. A collaborator who doesn’t have the @local package sees plain source and no errors. To share forms with everyone on a project, open the Typst packages dialog, turn on Use packages from the project folder and choose Vendor packages into project. The package, and its forms, then travel in the project’s typst-packages folder (see Typst projects).