Themes

A theme decides how a deck looks: its colors, its type, and the way each slide arranges what you put on it. Every Mosaic deck uses a theme, even one that never mentions the word. Import the package without naming a theme and you get the default theme. Name a different one and the whole deck changes at once, without a single edit to your slides.

Selecting themes

Five themes ship with the Mosaic package, and each is a design voice rather than a color scheme: it chooses its own title, section, and content arrangements, its own type, and its own slide furniture.

defaultThe quiet baseline. Plain sans type with the accent kept functional: blue bullets and links, an accent baseline rule on sections, and a small progress ring on numbered slides.
editorialThe magazine. Serif display over a sans body, a kicker masthead title under a strong opening rule, ghost-numeral sections, kicker rules under headings, and a folio in the corner.
metropolisThe homage to the beamer classic. Inverted header bar, and a progress bar that fills across section slides and along the bottom edge of every slide.
manifestoThe poster. One red on warm white, serif set large, uppercase tracked headings, a bordered title plate, rule sections, and a red progress ring.
monoThe terminal. Monospace throughout, dark by default, a \$ prompt on every heading, panel code blocks, toc sections, and a statusline with a slide counter.

There is no dark theme, because darkness is a palette rather than a design; the Colors section below flips a deck with one line. Import a theme as m and the whole deck follows it:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.metropolis as m

#show: m.setup.with(
  title: [The Optimal Number of Naps],
  subtitle: [Findings from a subject who slept through the study],
  authors: [Ada Lovelace],
  date: [March 2026],
)

#m.slide("title")

== First slide

A theme is a one-line decision.

The imported theme provides the same m.slide, m.layouts, m.components, notes, pauses, and incremental commands. Every theme exposes an identical API. Moving a deck from one theme to another is a one-line edit at the import.

The default import is the bundled default theme. These two spellings are the same deck:

// Default spelling
#import "@preview/mosaic:0.0.1" as m
// Explicit spelling
#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m

The two decks below are that same file. Only the theme on line two differs:

The small example deck under the default theme, first frame of 2 Default
The small example deck under the default theme

Loading slideshow…

Page 1 of 2
Open PDF
The small example deck under the metropolis theme, first frame of 2 Metropolis
The small example deck under the metropolis theme

Loading slideshow…

Page 1 of 2
Open PDF

Running example

The examples below all render the same short deck: a title slide with two authors, four content slides, a section slide, an image slide, and a few components along the way. Only the theme around it changes. So anything you see move, resize, or change color is the theme’s doing rather than the content’s.

That deck lives in one shared file, which exports a single name. deck takes the theme facade as its one argument, so the same body calls m.slide, m.layouts, and m.components whatever theme the example imported as m:

// _tour-deck.typ
#let deck(m) = {
  m.slide("title", numbered: false, title: [The Optimal Number of Naps], ..)

  m.slide(layout: m.layouts.content(variant: "header-body"))[
    == The problem
  ][
    Nobody agrees on how many naps a day is correct. We asked the only expert
    on the subject and he fell asleep during the question.
  ]

  m.slide("section", cells: (section: [The instrument]))

  // four more slides
}

Each example imports that one name and ends with #deck(m), which keeps the listing down to the lines worth reading.

Bundled themes

Five short wrappers each import a different theme and render the running example:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m
#import "_tour-deck.typ": deck

#show: m.setup

#deck(m)

Everything that differs between those five decks is what a theme owns. It is exactly four things:

The Customizing section below takes the first three in turn, each on a bundled theme. Open any deck to page through it:

The bundled default theme, first frame of 8 Default
The bundled default theme

Loading slideshow…

Page 1 of 8
Open PDF
The bundled editorial theme, first frame of 8 Editorial
The bundled editorial theme

Loading slideshow…

Page 1 of 8
Open PDF
The bundled metropolis theme, first frame of 8 Metropolis
The bundled metropolis theme

Loading slideshow…

Page 1 of 8
Open PDF
The bundled manifesto theme, first frame of 8 Manifesto
The bundled manifesto theme

Loading slideshow…

Page 1 of 8
Open PDF
The bundled mono theme, first frame of 8 Mono
The bundled mono theme

Loading slideshow…

Page 1 of 8
Open PDF

Customizing

A deck reaches every part of its theme through setup, without writing a theme of its own: colors: for the palette, ordinary set and show rules for the type, and layouts: for the arrangements. That is as far as most decks need to go.

Colors

A theme’s palette is one flat dictionary of eight colors; the Colors page describes what each entry paints. Pass colors: to setup to repaint any of them. The override is partial. Naming one color keeps the rest of the theme’s palette:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m
#import "_tour-deck.typ": deck

#show: m.setup.with(
  colors: (
    canvas: rgb("#fdf6ee"),
    surface: rgb("#fffdfa"),
    text: rgb("#2b1d12"),
    muted: rgb("#8a7462"),
    line: rgb("#ead9c6"),
    accent: rgb("#b4530a"),
  ),
)

#deck(m)

Both decks below run the default theme. Only the one on the right passes a dictionary to the colors argument. That dictionary reaches components too. A callout, card, or badge names a palette entry through its role:. The accents and status colors on the last slide follow the same override.

The default theme on its own palette, first frame of 8 Default
The default theme on its own palette

Loading slideshow…

Page 1 of 8
Open PDF
The default theme on a warm palette, first frame of 8 Custom palette
The default theme on a warm palette

Loading slideshow…

Page 1 of 8
Open PDF

Dark decks

Polarity is a palette, not a theme. The package bundles one dark palette, and passing it to colors: flips any theme to a dark deck; everything adapts on its own, as the Colors page explains:

#import "@preview/mosaic:0.0.1" as m
#import "_tour-deck.typ": deck

#show: m.setup.with(colors: m.palettes.dark)

#deck(m)

The two decks below are the default theme’s tour deck once more, identical except for that one colors: line:

The default theme on its own light palette, first frame of 8 Light palette
The default theme on its own light palette

Loading slideshow…

Page 1 of 8
Open PDF
The default theme on the bundled dark palette, first frame of 8 Dark palette
The default theme on the bundled dark palette

Loading slideshow…

Page 1 of 8
Open PDF

Beyond the polarity pair, the bundled palettes collection composes with every theme the same way; the Colors page lists it and renders the default theme under each of its palettes.

Typography

A theme’s typography is ordinary set and show rules. Rules you write after #show: m.setup layer on top of the theme’s. There is no separate typography system:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m
#import "_tour-deck.typ": deck

#show: m.setup

// Native rules, written after `setup`, layer on top of the theme's own.
#set text(font: "Libertinus Serif", size: 22pt)
#show heading: set text(weight: "regular", style: "italic")
#show label("mosaic-cell-section"): set text(weight: "regular")
#show label("mosaic-title-display"): set text(weight: "regular")

#deck(m)

The styling page covers those rules in full. The label reference lists every label a slide emits. A theme writes its own rules against that same list.

Rules written that way stay in the deck that holds them. A theme’s apply is the same list of rules in a function. Moving them there leaves the result identical. It also makes them portable. Run the base theme’s apply first to keep the rules it already carries:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m
#import "_tour-deck.typ": deck

// The same four rules, carried by a theme instead of the deck. `apply` runs the
// default theme's own rules first, then layers these on top.
#let serif = m.definition + (
  name: "Serif",
  apply: (body, colors: (:), options: (:)) => {
    show: (m.definition.apply).with(colors: colors, options: options)
    set text(font: "Libertinus Serif", size: 22pt)
    show heading: set text(weight: "regular", style: "italic")
    show label("mosaic-cell-section"): set text(weight: "regular")
    show label("mosaic-title-display"): set text(weight: "regular")
    body
  },
)

#show: mosaic.themes.setup(serif)

#deck(m)
The default theme on its own type rules, first frame of 8 Default
The default theme on its own type rules

Loading slideshow…

Page 1 of 8
Open PDF
The default theme under four added type rules, first frame of 8 Custom type
The default theme under four added type rules

Loading slideshow…

Page 1 of 8
Open PDF

Layouts

Every theme supplies three configurable slide layouts: content, title, and section. To customize them, pass a layouts: dictionary to setup. A replacement is either another built-in layout or a grid of your own:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.default as m

#import "_tour-deck.typ": deck

// A section slide of our own: a rule across the top, the title beneath it.
#let chapter = m.grids.rows(
  m.grids.track(auto, m.grids.cell("rule", content: line(length: 100%, stroke: 3pt))),
  "section",
)

#show: m.setup.with(
  layouts: (
    title: m.layouts.title(variant: "bordered"),
    section: chapter,
  ),
)

#deck(m)

Explicit m.slide(...) commands and automatic heading slides both select from that dictionary. Set it once and the whole deck follows. The Slides pages cover layouts in full.

The deck on the right borrows a built-in title variant that the default theme does not normally use. Its section layout exists only in that deck:

The default theme on its own layouts, first frame of 8 Default
The default theme on its own layouts

Loading slideshow…

Page 1 of 8
Open PDF
The default theme on a borrowed title layout and a custom section layout, first frame of 8 Custom layouts
The default theme on a borrowed title layout and a custom section layout

Loading slideshow…

Page 1 of 8
Open PDF

Writing

Everything above lives in the deck that uses it. Move on when you start copying the same three changes between decks. A theme is the dictionary that names them.

Themes are dictionaries

Bind one in the deck itself to see the whole shape at once. A theme needs only colors:

#import "@preview/mosaic:0.0.1" as mosaic

#let mine = (
  name: "Mine",
  colors: mosaic.palettes.light + (accent: rgb("#a23b72")),
  apply: (body, colors: (:), options: (:)) => {
    set text(font: "EB Garamond", size: 26pt)
    show heading: set text(fill: colors.accent)
    body
  },
)

#show: mosaic.themes.setup(mine)

Read mosaic.themes.setup carefully, because this page uses the name setup for two different things. mosaic.setup is the document show rule you apply to a deck. mosaic.themes.setup sets up nothing on its own. It takes a definition and returns a show rule of that kind. Its three keys map exactly onto the three sections above:

colorsThe palette. The same flat dictionary setup takes.
applyThe rules. The same set and show rules, in a function.
layoutsThe layout choice. The same dictionary setup takes.

Three further keys carry the rest of a theme:

nameThe theme’s display name, which its error messages quote. Defaults to "Custom".
defaultsOrdinary setup options the theme presets, such as spacing or overflow. A deck can still override any of them.
optionsThe theme’s own options and their defaults, covered below.

What apply owns

Mosaic’s engine emits no slide typography of its own. Every rule a slide renders with comes from the active theme. So a theme built from scratch states its whole look in apply, down to the labels a slide emits. The label reference is the checklist: a theme that styles mosaic-cell-title but forgets mosaic-cell-section renders section slides in the engine’s bare defaults, and nothing warns about it.

A theme may omit apply. It then changes colors and layouts only, and every slide falls back to Typst’s own text defaults. A theme derived from a bundled one inherits that theme’s rules and can leave apply alone.

The engine keeps two rules of its own, on <mosaic-note-heading> and <mosaic-note-body>. They style the printed speaker and notes pages. That furniture must read black on white whatever the theme does. A theme or deck rule on the same label overrides them.

Customize a theme

Every bundled theme exports the definition behind its own setup. Varying a bundled look is therefore an ordinary dictionary merge:

#import "@preview/mosaic:0.0.1" as mosaic
#import mosaic.themes.metropolis as base

#let mine = base.definition + (
  name: "Mine",
  colors: base.definition.colors + (accent: rgb("#0f766e")),
)

#show: mosaic.themes.setup(mine)

Two details decide whether that merge does what you meant.

The merge replaces a key outright. Writing colors: (accent: ..) would hand the engine a palette of one color rather than a recolored one. That is why the example merges into base.definition.colors instead. The same applies to layouts and options.

Replacing apply discards the base theme’s entire look, since apply is the look. To add rules on top of an inherited one, run the base theme’s own rules first, as the Typography section does.

Only the root package exports mosaic.themes. A bundled theme such as mosaic.themes.metropolis exports setup, layouts, components, and definition, but not the themes namespace itself. Deriving from one therefore means importing the root package as well, as above.

Shipping it

Once a theme is worth reusing, give it a file of its own. That file re-exports the shared Mosaic API alongside the theme’s setup and layouts. A deck then imports it exactly as it would a bundled theme, and never learns that it is not one:

// mytheme.typ
#import "@preview/mosaic:0.0.1": slide, note, fit, surface, grids, steps, components, themes
#import "definition.typ": definition
#import "layouts.typ" as layouts
#let setup = themes.setup(definition)

Those are the names a deck expects to find: setup and layouts at least, plus whatever else it uses from m. themes.setup validates the definition on the spot. A malformed theme therefore fails on that line, rather than somewhere inside a deck that imports it.

The deck below imports a complete theme in that shape. Its three files sit beside the deck in the repository: the importable file, the definition dictionary, and the layout functions.

// A custom theme is imported exactly as a bundled one would be.
#import "/docs-src/examples/embedded/appearance/_starter-theme.typ" as m
#import "_tour-deck.typ": deck

#show: m.setup

#deck(m)

It is the running example once more, this time under a theme of its own:

The default theme on its own look, first frame of 8 Default
The default theme on its own look

Loading slideshow…

Page 1 of 8
Open PDF
The starter theme, first frame of 8 Starter theme
The starter theme

Loading slideshow…

Page 1 of 8
Open PDF

Theme-specific options

options declares choices Mosaic itself knows nothing about. The theme below declares one, density, and reads it in apply:

// A theme with one option of its own, in the importable shape a deck expects.
#import "@preview/mosaic:0.0.1": (
  slide, note, fit, surface, grids, steps, components, themes, layouts,
)
#import "@preview/mosaic:0.0.1" as mosaic

#let base = mosaic.themes.default.definition

// `density` is the theme's own option. Mosaic knows nothing about it: the name
// simply becomes a named argument of the setup below, and its resolved value
// reaches `apply`.
#let seminar = base + (
  name: "Seminar",
  options: base.options + (density: "airy"),
  apply: (body, colors: (:), options: (:)) => {
    let size = if options.density == "airy" { 30pt } else { 22pt }
    show: (base.apply).with(colors: colors, options: options + (base-size: size))
    body
  },
)

#let setup = themes.setup(seminar)

Each name in options becomes a named argument of the setup the definition produces. A deck passes a value, or says nothing and takes the theme’s default:

// `density` is the theme's option, so its setup accepts it as a named argument.
#import "_option-theme.typ" as m
#import "_tour-deck.typ": deck

#show: m.setup.with(density: "dense")

#deck(m)

Mosaic hands the resolved options to layouts as well as apply, so layouts may be a function of them rather than a fixed dictionary. That is what lets one option offer genuinely different arrangements rather than only a different type size.

An option name that collides with a built-in setup option raises an error where you bind the theme. Every other named argument a deck passes falls through to setup as usual.

The theme authoring API documents every definition key in full.

The seminar theme at its default density, first frame of 8 Default
The seminar theme at its default density

Loading slideshow…

Page 1 of 8
Open PDF
The seminar theme at its dense setting, first frame of 8 density: dense
The seminar theme at its dense setting

Loading slideshow…

Page 1 of 8
Open PDF