Themes¶
A theme is a Python class.
You subclass Theme, give it a typed color Palette (and optional Typography),
and pass an instance to your deck.
Because a theme is an ordinary class,
it can live in your deck.py, in a local module, or in an installed package —
so themes are importable and shareable, no matter how they were installed.
Using a theme¶
from inkflow import Deck
from inkflow_themes import Nord # some installed theme package
Deck(theme=Nord())
Set no theme and you get the built-in Catppuccin theme:
Defining a theme¶
Subclass Theme and set class attributes:
from dataclasses import replace
from inkflow import ColorMode, Palette, Theme, Typography
class Sunset(Theme):
mode = ColorMode.DARK
dark = Palette(bg="#2b1b2f", text="#ffe6d5", accent="#ff7a59")
light = replace(Theme.light, accent="#d1495b")
typography = Typography(heading_font="Fraunces", heading_weight=700)
darkandlightare aPaletteeach — one per color mode.typographyis aTypography.- Every token has a sensible default (a neutral floor), so you set only the ones you care about.
The token API¶
The theme's job is to supply values for a fixed set of --inkflow-* CSS custom
properties that the layouts and rendered Markdown consume.
inkflow always loads a contract stylesheet that provides the structural rules and
the markdown element styling and reads those tokens;
your theme provides the values.
So a theme that sets nothing still renders — you override only what you want.
There are two token groups:
a Palette of colors, one instance per color mode,
and a single Typography for fonts and text metrics.
Each field maps to a CSS variable by kebab-casing its name
(text_muted → --inkflow-text-muted).
Every field, its default, and what it styles is listed in the Themes reference.
Heading sizes are a fixed scale in the contract, not tokens.
Font names are font-family values,
so ship the font file in your theme's fonts/ directory to embed it
(see the Fonts guide).
Overriding only some tokens¶
A Palette's field defaults are the neutral dark floor,
so Palette(accent="#88c0d0") gives that accent plus the dark floor for everything
else — correct for a partial dark palette.
For a partial light palette, start from the light floor with dataclasses.replace,
because a bare Palette(...) would fill the unnamed fields with the dark floor:
class Nord(Theme):
dark = replace(Theme.dark, accent="#88c0d0") # dark floor + accent
light = replace(Theme.light, accent="#5e81ac") # light floor + accent
A full custom palette just names every field:
Palette(bg=..., text=..., accent=..., ...).
Color mode, font size, transition, and overlays¶
Deck-level mode, font_size, transition, and overlays default to
"defer to the theme".
Resolution runs slide → deck → theme:
Deck(theme=Nord()) # mode/size/transition/overlays come from Nord
Deck(theme=Nord(), mode=ColorMode.LIGHT) # deck overrides the theme's mode
overlays is how a theme ships its own branding.
Set it on the theme class and every deck using that theme gets the chrome,
with Deck(overlays=[]) or Slide(overlays=[]) opting back out:
See Overlays for how they compose.
mode sets the data-theme attribute the presenter reads:
ColorMode.DARK leaves it empty (the :root palette applies) and
ColorMode.LIGHT sets data-theme="light" (the light palette applies).
Built-in layouts, recolored¶
A theme needs no layout files of its own.
Bare layout names in Slide(...) resolve through the project, then the theme,
then the built-in layouts,
and the built-ins take your palette automatically
because they paint through the inkflow-fill-* token classes.
Ship your own layouts/*.svg only when you want different geometry:
you can override just the layouts you care about and inherit the rest.
Because any theme can fall through to them,
the built-in layouts' own stylesheet (zone alignment and heading sizes,
keyed on .layout-<name>) is loaded for every deck,
not only for decks on the built-in theme.
Your theme's styles.css loads after it,
so restyling a built-in layout is a matter of naming the same selector:
Shipping a theme as a package¶
A theme locates its assets from its own module, so an installed theme just works.
Put a theme/ directory next to the module that defines the class:
inkflow_themes/
__init__.py # class Nord(Theme): ...
theme/
layouts/*.svg # optional — only layouts you add or override
overlays/*.svg # optional — chrome referenced by Theme.overlays
styles.css # optional — CSS the token API doesn't cover
fonts/*.woff2 # optional — bundled fonts
Then from inkflow_themes import Nord; Deck(theme=Nord()).
Themes must be installed as regular (unpacked) packages;
a zip-imported theme raises a clear error.
SVG element utility classes¶
SVG elements can carry semantic classes that follow the active color mode. Each token has a fill and a stroke variant, e.g.:
Available for these tokens: inkflow-fill-<token> and
inkflow-stroke-<token> (bg, surface, border, text, text-muted, accent,
accent-fg, code-bg, code-text, and the named colors red … grey).
The presenter's light/dark switch updates all of them automatically.
Inkscape cannot read CSS custom properties, so a classed element looks unstyled in the editor until you install the palette and refresh the preview. See theme colors in the editor.
Per-deck and per-slide CSS¶
Beyond the theme you can inject CSS at two levels.
A bare string is read as a path to a CSS file,
so wrap literal CSS in Inline(...):
from inkflow import Deck, Inline, Slide
Deck(style=Inline('text { font-family: "Inter", sans-serif; }')) # every slide
Slide("title", extra_style=Inline("#headline { fill: hotpink; }")) # one slide
Deck(style="styles/deck.css") # or point at a file, relative to deck.py
Slide style beats deck style, which beats the theme. The full stylesheet order, each layer overriding the ones before it:
- the contract stylesheet (structural rules, markdown elements)
- the active theme's tokens
- the built-in layout styling
- the active theme's
styles.css - the project's
styles.cssnext todeck.py Deck(style=...), thenSlide(extra_style=...)
Project files¶
Two files next to deck.py are picked up automatically, with no wiring:
| File | Used for |
|---|---|
styles.css |
Deck-wide CSS, and @keyframes for custom animations |
scripts.js |
JavaScript, including custom transitions |
styles.css sits at level 5 of the cascade above,
so it overrides the theme and is overridden by the per-deck and per-slide styles.
A theme can ship its own pair of both, which load before the project's.
Font size¶
Deck(font_size=36) (default from the theme, 36) sets the base font-size on each
zone's <foreignObject> root;
all em/rem units cascade from it.
Set it per slide with Slide(..., font_size=48).