Skip to content

The Four Outputs

One source file, one compile per output. The HTML target is detected automatically with target(); the paged modes are selected with --input animo=, and default to the handout.

# HTML presentation
typst compile --format html --features html talk.typ talk.html

# Presentation PDF (one page per subslide)
typst compile --input animo=presentation talk.typ talk-presentation.pdf

# Handout PDF (final state per slide), the default for paged output
typst compile talk.typ talk-handout.pdf

# Handout SVG (one file per page)
typst compile -f svg talk.typ 'talk-{p}.svg'

A mistyped mode is an error rather than a handout that looks like a success.

Multi-page SVG export fails without a page number template in the output path, which is why the last command line is the odd one out. {p} is the page number and {0p} is the same number padded to the page count.

Add --ignore-system-fonts to every command to make a rendering reproducible between machines. Animo itself uses only the fonts typst embeds.

What Each Output Is

Output What it shows
HTML presentation the deck, animated, in a browser
Presentation PDF one page per step, for a venue without a browser
Handout PDF one page per slide, for printing
Handout SVG the handout pages, for embedding in another document

A slide with no sub call has one state, so it is one page in both paged outputs. The two stop being the same as soon as a slide has subslides: the presentation gets a page per step and the handout keeps the states that asked for one.

Presenting and the Live Preview

The keys, the position in the URL and the typst watch loop are on their own page: Presenting.

An Example

The deck the test suite compiles to all four outputs on every build:

examples/tour.typ
// SPDX-FileCopyrightText: 2026 Toon Verstraelen <Toon.Verstraelen@UGent.be>
// SPDX-License-Identifier: Apache-2.0

#import "@preview/animo:0.1.0": *

// The deck's shape is written once, as a document-level show rule.
// It is also what emits the HTML page, its stylesheet and its runtime,
// and what sets the page size of the two paged outputs.
#show: animo.with(width: 16cm, height: 9cm, margin: 1cm)

#slide(numbered: false)[
  #set align(center + horizon)
  #text(size: 2em)[*Animo*]

  A slide is a viewport onto a canvas.
]

#slide[
  = The viewport

  The viewport is what the audience sees:
  one HTML slide container, one presentation page, one handout page.
  It is the size of the deck, and it clips.

  Whatever falls outside it is not carried over to a next slide.
]

#slide(background: rgb("#eef2ff"))[
  = A background

  A colour becomes the page fill on paper and a CSS background in the browser,
  because `set page` is unavailable in the HTML target.
]

#slide[
  = The canvas

  The canvas is what the body is laid out on.
  It is at least as large as the viewport, and the placement below makes it wider:
  the word after this paragraph sits four centimetres beyond the right edge,
  so it is on the canvas and not on the slide.

  #place(dx: 17cm, dy: 2cm)[Out of view, for now.]

  Panning brings it into view, and panning is what a later version adds.
]

#slide[
  = Placing things

  Animo has no header, footer or templating machinery.
  A recurring element is `#place` inside a wrapper around `#slide`.

  #place(bottom + right)[#text(size: 0.7em, fill: gray)[a placed corner mark]]
]

#slide(animation: {
  // The primitives are imported inside this block, so `move`, `scale` and `hide`
  // keep their built-in meaning everywhere else, the slide body included.
  import anim: *
  sub(reveal("second"))
  sub(move("first", x: 2cm), scale("second", 1.3))
})[
  = Tags and a timeline

  The body says what is on the slide and tags the parts a timeline may address.
  The animation argument says when and how those parts move.

  #tag("first")[This line slides to the right on the last step.]

  #tag("second", hidden: true)[This one starts out invisible.]

  In the browser each step takes 400 ms, forwards as well as backwards.
  The presentation PDF has one page per step and no motion at all.
]

#slide(animation: {
  import anim: *
  // Every occurrence of a name moves together, and operations accumulate:
  // the square ends up three centimetres to the right and twice as large.
  sub(move("box", x: 2cm, y: 1cm))
  sub(move("box", x: 1cm), scale("box", 2), hide("gone"))
})[
  = What the browser animates

  #tag("gone")[This line fades out on the last step, and keeps its space.]

  #place(dx: 2cm, dy: 3cm, tag(
    "box",
    wrap: box,
    rect(width: 2cm, height: 2cm, fill: rgb("#4f46e5")),
  ))

  #place(bottom + left)[
    #text(size: 0.7em, fill: gray)[
      A scale is about the element's own centre, and nothing reflows around it.
    ]
  ]
]