A document theme is one Typst file under .ntropy/themes/typst/ that
gives a vault's PDFs their own look. It redefines what it wants and
inherits the rest. This page is the reference: what a theme may
override, what it receives, what the note's body is made of, fonts,
assets, errors, and the paper size.
Writing a document theme
builds one step by step.
The theme file
A theme named corporate is the file
.ntropy/themes/typst/corporate.typ, selected with theme = "corporate"
in the [render] table of .ntropy/config.toml; selecting is described
in Themes. ntropy init does not
create the directory, so make it first:
mkdir -p .ntropy/themes/typst
From then on every ntropy render in the vault uses the theme, with
nothing extra on the command line. A configured theme with no file fails
before the vault is scanned, with an error naming the path it looked
for. A theme that does not compile fails with typst's error, described
under Errors and warnings. ntropy ships no named
themes of its own beyond the built-in default; themes are the vault's
own files.
What a theme overrides
This theme puts a logo in the page header and drops the metadata strip,
so internal tags and status: draft never reach a customer:
// .ntropy/themes/typst/corporate.typ
#let note(title: none, frontmatter: (:), paper: "a4", body) = {
set document(title: title) if title != none
set page(
paper: paper,
margin: (x: 2.2cm, top: 3.4cm, bottom: 2.4cm),
header: {
align(right, image("/assets/logo.svg", width: 3.2cm))
v(-0.4em)
line(length: 100%, stroke: 0.6pt + rgb("#2dd4bf"))
},
)
set text(size: 11pt)
if title != none {
text(size: 1.6em, weight: "bold", title)
v(0.8em)
}
// No frontmatter strip: nothing but the body below the title.
body
}
Four functions are yours to override; a theme defining only note keeps
the built-in look for the others:
| Function | Signature | Renders |
|---|---|---|
note |
note(title: none, frontmatter: (:), paper: "a4", body) |
the whole document |
callout |
callout(kind: "note", body) |
a > [!NOTE] admonition |
notelink |
notelink(body) |
the text of a note link: the target's title, or the link's text when it carries a heading anchor and has text |
task |
task(done: false) |
a task-list checkbox |
The theme's source is spliced into the emitted document after ntropy's
own definitions and before the document applies note. Typst binds a
name to the last definition above its use, which is why a theme's
note wins and a function the theme leaves alone keeps the built-in
one. The built-in definitions also include the helpers fmt-value,
is-empty, and callout-styles, which a theme may call; redefining one
of them does not change the built-in note or callout.
The built-in note is more than the title block: it also holds the rules
that style headings, code, quotes, tables, links and rules. A note
written from scratch, like the one above, drops all of them, and the body
comes out in typst's plain defaults. Starting from a copy of the built-in
note keeps them;
Writing a document theme
shows how.
A theme needs no note of its own for a smaller change. set and show
rules at the top of the theme file apply to the whole document and keep
the built-in layout:
// .ntropy/themes/typst/plex.typ
#set text(font: "IBM Plex Sans")
#show heading: set text(fill: rgb("#0f766e"))
Where the built-in note sets a property itself, its rule comes later
and wins: the text size, the paragraph justification and the page margins
change only in a note of your own.
What note receives
title: the note's title as a string.frontmatter: the note's frontmatter as written,titleandtagsincluded, as Typst values. Strings, integers, floats, booleans andnullbecome Typst strings, integers, floats, booleans andnone; lists become arrays and mappings dictionaries, at any depth. A YAML date such as2026-11-01stays a string, and a value with a YAML tag (!degrees 90) becomes the string of its YAML text, tag included.frontmatter.at("author", default: none)reads a field a note may lack. The note's date, which lives in its file name, is not passed.paper: the paper size, see Paper size.body: the converted note.
The body leaves out a level-one heading that opens it and reads exactly
the note's title, as every note made from ntropy's default template does; the
title is note's to draw. A note that does not draw title therefore
shows no title at all. The PDF outline has no entry for that heading, and
a theme that numbers headings counts the sections of such a note from
0.1.
What the body is made of
The body is plain Typst, so a theme styles it with set and show rules
on these elements:
| Markdown | Typst element |
|---|---|
# to ###### headings |
heading, levels 1 to 6 |
| paragraphs | paragraphs separated by blank lines |
- item |
list |
1. item |
enum, starting at the list's first number and counting up from it |
- [ ] task |
a list item that starts with task(done: ...) and a space; the list's bullet stays unless the theme changes list |
> quote |
quote(block: true) |
> [!NOTE] and the other four kinds |
callout(kind: "note"), the kind in lower case |
| pipe table | table with a table.header; each cell of a column with a Markdown alignment is a table.cell(align: ...), and no align: is passed |
| fenced or indented code | raw block, with the fence's language |
`code` |
raw inline |
*em*, **strong**, ~~gone~~ |
emph, strong, strike |
[text](url), bare URLs |
link |
| a note link | link to <slug>.pdf around notelink, or link to the heading's label when the link names a heading of the note itself |
 with a local path |
image with a path from the vault root and alt: set to the alt text, left out when the alt text is blank |
[^label] |
footnote |
--- |
line(length: 100%) |
| a hard line break | linebreak() |
A column with a Markdown alignment keeps it, as every one of its cells
sets align itself. A column without one follows
set table(align: ...); the built-in look sets start. A quote with any
other marker, such as > [!FOO], stays a quote, so callout only ever
sees the five kinds. Raw HTML is dropped, with a warning naming it.
The built-in look also measures every column and sets the column widths itself, so cells do not run into each other. Widths come from the text of the cells, so an image in a cell counts as no width.
- When the widest word of each column does not fit the page, the table is set in smaller text, down to 60 percent of the size.
- A table that would need smaller text stays at 60 percent, runs past
the right edge of the page and loses the columns beyond it. A PDF
render warns about it, and
--strictturns the warning into a failure. --to typstgives no such warning, since the check runs typst on the document after the PDF compile. If that check cannot run, the render warns that it could not check the tables and still finishes.- A theme that wraps tables in a padded block takes that padding out of the columns of a squeezed table.
Every footnote is drawn once, at its first reference in the text, and
every later reference shows the same number. Headings carry labels that
start with ntropy-h-, and footnotes carry labels that start with
ntropy-fn-. Do not define labels with these prefixes. A theme must not
draw the body twice, nor a block that holds a heading or a footnote with a
repeated reference: Typst rejects the duplicated label as soon as a link
names that heading or a later reference names that footnote, and the
compile fails. An outline() of the headings compiles, unless a heading
holds a footnote the note references again. A show footnote rule runs
for the repeated references as well, and it.body is a label there, so a
rule that prints the body must check type(it.body) == label.
Fonts
The built-in look sets its text in the fonts typst brings along:
Libertinus Serif, New Computer Modern and DejaVu Sans Mono. The tick in a
done task box is drawn as a shape, so it needs no font. Libertinus Serif
covers Latin, Greek, Cyrillic and Hebrew. For a script it does not cover,
such as Arabic or Japanese, typst takes the glyphs from a font it finds on
the machine, so the PDF looks different from machine to machine. Without
such a font typst falls back to DejaVu Sans Mono for Arabic, which has a
monospace look, and Japanese characters become empty boxes, with no warning
from ntropy, also under --strict.
A theme makes such a script look the same everywhere by naming a font for it, after the one it already uses:
#set text(font: ("Libertinus Serif", "Noto Serif CJK JP"))
The font must be installed or found through TYPST_FONT_PATHS, as below.
A theme can use every font typst finds: the fonts installed on the system,
and the fonts in the directories named by the TYPST_FONT_PATHS
environment variable. ntropy passes typst no font directory of its own,
so a font file in the vault is found only when TYPST_FONT_PATHS points
at it. A relative path there is resolved against the directory you run
ntropy in, so an absolute one is safer:
export TYPST_FONT_PATHS="$(ntropy info -p)/assets/fonts"
typst fonts lists the names typst can use, and
typst fonts --font-path <dir> includes a directory.
A font name typst does not find falls back to the default font. The
render still succeeds, with typst's warning unknown font family: <name>
as one of its warnings, and --strict fails on it. pdffonts from
poppler lists the fonts a PDF embeds.
Assets and the vault root
The logo above lives at <vault>/assets/logo.svg, outside all-notes/,
which holds notes and nothing else. typst compiles the document as if it
sat at the vault root, the root it resolves paths against. A path in
a theme is therefore read from the vault root whether it starts with /
or not: /assets/logo.svg and assets/logo.svg name the same file.
A theme can split its code into more files and import them by such a
path, #import "/.ntropy/themes/typst/colors.typ": accent. An imported
file is a module of its own: it sees neither ntropy's definitions such as
fmt-value nor the names of the theme file. Pass it what it needs as
arguments, or keep the code that uses them in the theme file.
Errors and warnings
A theme that does not compile fails the render with typst's error, which
names where the problem is. <stdin> with a line number means a line of
the emitted document, the file --to typst writes: the theme starts after
ntropy's own definitions, so that line is further down than in the theme
file. An error in a file the theme imports names that file and its own
line.
A compile that succeeds can still print warnings, such as an unknown font
family. A PDF render shows each of them as one warning of ntropy, and
--strict fails on it. The line holds typst's message, the source line it
points at in backticks and typst's hints, if it has any:
warning: unknown font family: no-such-font (in `set text(font: "no-such-font")`)
The line number is left out, as it counts lines of the emitted document. typst prints nothing for a character no font covers, so that case stays silent.
The --to typst format runs no compiler and shows no typst warnings.
Compiling its artifact by hand prints them, and shows the emitted line an
error names. Since a theme's assets are addressed from the vault root,
the compile takes the vault as its root. typst refuses a document file
outside that root, so hand the artifact over on standard input, as
ntropy does:
typst compile --root <vault> - note.pdf < note.typ
To see a theme draw the parts of a note in one document, render the theme test note, described under The theme test material.
Paper size
Rendering defaults to a4. A [render] section in the vault's
.ntropy/config.toml picks a different format:
[render]
paper = "us-letter"
Supported values: a3, a4, a5, iso-b5, jis-b5, us-letter,
us-legal, us-tabloid, us-executive, us-oficio. An unknown value
is a config error naming the bad name, reported before anything renders.
The value reaches the theme as the paper argument of note, so a theme
whose set page does not pass it on ignores the setting.