deckoction
Full documentation: slides-md.readthedocs.io
Turn a single Markdown file into a static HTML slide deck. No client-side framework —
just the same vanilla HTML/CSS/JS approach as the hand-built sulfur_slides
deck it's based on: a dark theme, keyboard/click navigation, click-to-zoom images, and
vendored KaTeX for math, with no build step or CDN dependency in the output.
Quickstart
uv tool install deckoction-md # or 'deckoction-md[pdf]' for --pdf/--thumbnail
deckoction slides.md # build next to slides.md and serve at :8000
From a source checkout, uv sync then uv run deckoction examples/demo/slides.md (add
--force to rebuild over a previous build) builds examples/demo/build/index.html (plus
assets/, vendor/katex/, and slide_images/) and serves it at http://localhost:8000.
examples/demo/slides.md exercises every layout and feature in one deck — a good starting
point to copy from.
Authoring format
Write one Markdown file (e.g. slides.md). Slides are separated by a line containing
exactly +++. Each slide has YAML frontmatter (fenced by ---/---) followed by a
Markdown body:
---
layout: content
kicker: Overview
title: Why a Markdown Slide Tool
image: images/diagram.png
image_alt: Diagram of the pipeline
---
- Author once in a single Markdown file
- YAML frontmatter per slide controls layout and media
- Generator assembles static HTML/CSS/JS
+++
---
layout: title
title: My Presentation
subtitle: A subtitle
author: Jane Doe
date: today
background: images/hero.jpg
---
The body supports paragraphs, up to two levels of bullet lists, inline **bold** /
*em* / inline code, inline KaTeX $...$ (rendered client-side, so any valid LaTeX
passes through untouched), and > blockquotes (rendered as an italic grey callout box,
matching the KaTeX formula box style — a quote inside a bullet list, either as its own
item (- > quote, shown without a bullet marker) or indented under a bullet, keeps the
same look, offset to the bullet's indent), and GFM pipe tables (| a | b | rows with a
|---|:-:| separator line; column alignment and inline math in cells both work). Don't use Markdown headings or ![]() images in
the body — use the title/kicker/image fields instead (the generator warns if it
sees either).
Anything wrapped in <!-- ... --> (or <!--- ... --->) is stripped before the file is
even split into slides, so it's never parsed or rendered — comment out a note-to-self
inside a slide's body, a YAML field, or a whole slide (fences included) and it's simply
gone from the build.
Layouts
layout |
What it renders |
|---|---|
title |
Centered title over a full-bleed background, in a translucent panel, with optional subtitle, author, date |
content |
title/kicker + bullets on the left, a single image, two images, video, or panels comparison on the right (default layout) |
stacked |
title/kicker + full-width bullets, no media column — also where block math (formula) usually goes |
split |
title/kicker + a full-width row of 2-3 panels (image + caption), no bullets |
image |
A single image fills the entire slide edge-to-edge, sized via fit (contain, default — fit to screen, no cropping; or cover — fill the screen, cropping if needed); title/kicker are optional, shown as a small overlay panel — omit both for a bare full-slide image |
references |
Renders the full numbered bibliography (see Citations & references) — no fields of its own to set |
Per-slide fields
| field | applies to | notes |
|---|---|---|
layout |
all | title / content / stacked / split / image / references; default content |
title |
all | required for title layout; optional caption for image |
kicker |
content/stacked/split/image | small uppercase label above the title |
subtitle |
all | a line under the title (on image, inside the caption overlay) |
author |
title | falls back to deck.yaml's default_author |
date |
title | literal string, or today to fill in the build date |
image / image_alt / image_label / image_reference |
content, image | on content, a boxed image with an optional caption image_label (mutually exclusive with video/panels/images/phase_in); on image, the full-bleed slide image (required unless phase_in is set, mutually exclusive with background/phase_images). Accepts PNG/JPEG/GIF/SVG, a placeholder name, or a .pdf file. image_reference cites a bibliography key, numbering it in the image's top-right corner |
images |
content | 2 or more {image, label?, alt?, reference?}; auto-arranged into rows in reading order from their aspect ratios (mutually exclusive with image/video/panels/phase_in) |
video |
content | mutually exclusive with image/panels/images/phase_in |
equation |
content | LaTeX (surrounding $...$/$$...$$ optional) shown as display math on a highlight box (accent-tinted; a theme can override equation-bg/equation-fg), scaled to fill the media column, in place of an image; mutually exclusive with image/video/panels/images/mermaid/phase_in |
mermaid |
content | a Mermaid diagram definition (flowchart, sequence, class, state, gantt, pie, ...) drawn in the media column in place of an image, colored to match the deck theme; usually written as a ` |
panels |
content, split | list of {image, label, alt?, reference?}; required for split |
background |
all except image |
image path (or .pdf); layers behind the slide content |
background_opacity |
all | 0.0-1.0, default 1.0 |
fit |
image | contain (default, fit to screen) or cover (fill the screen) |
formula |
content/stacked | raw LaTeX (no $$), rendered in a .formula-box |
formula_note |
content/stacked | small caption under the formula |
notes |
all | speaker notes, emitted as an HTML comment (not visible) |
id / classes |
all | override the slide's id, or add extra CSS classes |
phase_in |
content, stacked, split, image | true to reveal the slide step by step — bullets on content/stacked, panels on split, phase_images on image; see Phase-in reveal. On content, mutually exclusive with image/video/images/panels |
phase_level |
content, stacked | with phase_in: true: which bullet-indent level drives the reveal (1 = top-level, default; 2 = first sub-level; ...) |
phase_images |
content, image | with phase_in: true: list of {image, alt?, reference?} shown one per reveal step. On content, it stays on the last image once the list runs out, and an entry can instead be {images: [{image, label?, alt?, reference?}, ...]} (2 or more) to show a group on that step (arranged like images), {equation: ...} to show an equation, or {mermaid: ...} to show a diagram on that step; on image, it's required and each entry is one full-bleed step |
Any slide can carry a background + background_opacity — this is independent of the
title slide's translucent panel, so a dimmed background works on content slides too.
A content slide's images (2 or more) are arranged into rows from each image's actual
pixel dimensions (read at build time, no dependency needed). Images keep their order,
filling rows left to right and then top to bottom; within a row they share one height and
span the full width. Of every possible way to break the list into rows, the build picks
the one that leaves the least blank space in the media column while keeping the images
roughly the same size — e.g. 3 squares sit in one row, 4 form a 2x2 grid, two wide plots
stack, two portraits sit side by side.
Every rendered <img> is click-to-zoom — clicking it opens a full-screen lightbox fit to
the screen. Inside the lightbox, click the image (or scroll the mouse wheel over it) to
zoom in further, centered on the cursor; click again to zoom back out. Click the dark
backdrop or press Escape to close.
Phase-in reveal
Setting phase_in: true on a content slide turns its body bullets into a build/reveal
sequence: the deck steps through them one at a time (each step is its own slide for
navigation and --pdf purposes, but they all share one number in the current / total
counter). Write the bullets exactly as usual — no special syntax:
---
layout: content
title: Rollout Plan
phase_in: true
phase_images:
- image: images/step1.png
- image: images/step2.png
---
- First we do X
- a supporting detail
- Then Y
- Finally Z
This produces 4 physical steps: bullet 1 revealed → bullet 2 revealed (bullet 1 dims) →
bullet 3 revealed (bullets 1-2 dim) → everything undimmed (the settled final view).
phase_images is optional and cycles one image per step, freezing on the last entry
once the list runs out — a single-entry list just keeps one static image on screen
throughout. On content, an entry can be - equation: $a^2+b^2=c^2$ instead of an
image, to show that equation in the media column on that step. Sub-bullets always reveal together with their parent bullet.
phase_level changes which indent level drives the reveal (default 1, top-level
bullets). phase_level: 2 reveals at the first sub-bullet level instead — a top-level
bullet with sub-bullets appears alongside its first sub-bullet, and a top-level bullet
with no sub-bullets at all just becomes its own step.
phase_in works on other layouts too:
stacked— reveals the body's bullets exactly as above (phase_levelincluded). Theformulabox stays visible on every step.split— reveals one panel per step. Earlier panels dim, and panels not shown yet keep their space so nothing shifts. A final step shows every panel at full brightness: 3 panels → 4 steps.image— cycles throughphase_imagesfull-bleed, one per step, in place of the singleimagefield. There's no extra final step: 3 images → 3 steps.title/kickerstay as the caption throughout.
---
layout: image
title: Time Series
phase_in: true
phase_images:
- image: images/t0.png
alt: Frame at t = 0
- image: images/t1.png
alt: Frame at t = 1
---
Citations & references
Set bibliography: in deck.yaml to a file of numbered references — a .bib file
(BibTeX, formatted per the American Chemical Society/ACS style — the only style
implemented so far) or a plain file of "key": "citation text" lines:
bibliography: refs.bib # or refs.md -- see below
Cite from a slide's body with [@key], or several at once with [@key1; @key2]:
The reaction proceeds via a radical mechanism [@smith2020].
References are numbered in the order they're first cited across the whole deck (not
per-slide), rendered inline as a small superscript [1] (linking to the paper's DOI
when the bibliography entry has one), collected as a small footnote at the bottom-left
of whichever slide cited them, and listed in full wherever you place a layout: references slide, typically the last one.
Cite from an image instead of body text with reference: key (images[]/panels[]
entries) or image_reference: key (the single boxed image, or the full-bleed image
layout's image) — its assigned number appears as a small badge in the image's top-right
corner.
BibTeX (.bib) files: a deliberately partial parser, not the full BibTeX spec —
article, book, incollection/inbook, and inproceedings entries are recognized
and ACS-formatted; anything else falls back to whatever of author/title/year is present.
Not supported: @string macros, cross-references, and LaTeX accent/escape sequences
({\'e} stays literal) — write accented characters directly as UTF-8 instead. Journal
names are used exactly as given, with no abbreviation lookup.
Plain (.md) files: one entry per line, the citation text used exactly as given
(including any HTML you write yourself, e.g. <i>...</i>) with an optional DOI as a
third field:
"smith2020": "Smith, J. A.; Doe, J. B. A Study of DNA Nanotechnology. J. Chem. 2020, 12, 34-56." : "10.1021/xyz123"
"jones2019": "Jones, A. Principles of Organic Synthesis. Academic Press: New York, 2019."
Citing a key that isn't in the bibliography, or citing anything at all with no
bibliography: configured, is a build error — without bibliography: set, [@...]-
shaped text is otherwise left completely untouched.
On a phase_in slide, the footnote list only shows references for whichever bullet,
panel or image is currently revealed at each step — not the whole slide's accumulated
citations — with the final "everything undimmed" step (if the layout has one) showing
the full combined list.
Mermaid diagrams
A content slide's mermaid field draws a Mermaid diagram in
the media column, in place of an image:
---
layout: content
title: Build pipeline
mermaid: |
flowchart LR
A[slides.md] --> B{parse} --> C[index.html]
---
Any Mermaid diagram type works (flowchart, sequence, class, state, ER, gantt, pie,
mindmap, ...). A phase_images entry can also be - mermaid: | to show a diagram on one
reveal step. Click a diagram to zoom it, like an image.
How it works: as with KaTeX, the diagram is drawn in the browser by a bundled copy of
Mermaid (no internet connection needed), which is copied into the output only when a
deck actually uses a diagram. Mermaid can't read CSS variables, so the deck's script
resolves the theme's colors and passes them to Mermaid's base theme: node fill
diagram-node-bg (default bg-panel), node text diagram-node-fg (default
panel-fg), node border diagram-border (default accent) and lines/arrows
diagram-line (default fg-muted); edge labels and other text use fg, and the
theme's body font is used throughout. A theme (or a theme: override dict) can set any
of those four keys if a diagram needs different colors. A diagram with a syntax error
shows Mermaid's error message in its place instead of breaking the deck. --pdf and
--thumbnail wait for diagrams to finish drawing.
Image formats
Image fields take PNG, JPEG, GIF or SVG files (plus .pdf, below). An SVG is copied
as-is and stays vector — sharp at any zoom and in --pdf output. For the images
arrangement its aspect ratio comes from the root <svg> tag's width/height (absolute
units like px/pt/in), or else its viewBox; an SVG with neither is a build error
there.
PDFs as images
Any image-path field (image, background, images[].image, panels[].image,
phase_images[].image) accepts a .pdf file too — no different syntax, just point at
a PDF the way you'd point at a PNG. Its first page is rasterized to a PNG at build time
(200 DPI, not configurable) and flows through the rest of the pipeline exactly like any
other image — same styling, same click-to-zoom, same aspect-ratio-based
images arrangement, same citation reference badges. This needs the pdf-images extra:
uv sync --extra pdf-images
A PDF referenced on multiple slides is only rasterized once. Only the first page is used; there's currently no way to pick a different page.
Placeholder images
Any image (on content or image layouts), background, or images/panels entry
accepts a LaTeX-mwe-style placeholder name instead of a real file path — no image asset
needed while drafting: example-image (a plain outlined box) or example-image-a
through example-image-z (the same box with that letter in it, e.g. example-image-c).
These are bundled with the package and copied into the build like any other referenced
image. Omitted alt text is auto-filled (e.g. "Placeholder image C"), so no warning
fires for a placeholder left without one.
Deck-wide config (optional)
A deck.yaml file next to your .md file (or passed via --config) sets page-wide
defaults:
title: My Presentation # <title> tag
default_author: Jane Doe # used when a title slide omits `author`
theme: alomancy # named preset — see below
bibliography: refs.bib # numbered citations — see Citations & references above
theme: accepts either of two forms:
-
A named preset (a single string) — swaps colors, the body font, and an optional watermark image in one line. Shipped presets:
Preset Look defaultThe original dark/gold theme (used automatically if theme:is omitted — no visual change either way)alomancyDark, with ALomancy's brand violet ( #6C2FBE) as the accent, the "Inter" font, and a subtle ALomancy logo watermark in the bottom-left corner of every slideneuefischeDark violet background with Neuefische's brand orange ( #f44717) as the accent, the "Poppins" font, and a subtle Neuefische logo watermarkfhiLight background with dark teal body text/panels, an olive-green ( #a5b631) accent, the "Poppins" font, and a subtle FHI logo watermark — the one light-background preset, so its title slide overridestitle-fg(see below) to stay legibleNew presets are added by registering a
ThemeDefinitionindeckoction/themes.py— see that module for the fields (colors,font_body,watermark). -
A raw dict of CSS custom-property overrides (the original form, still fully supported) — layers only the keys you set on top of
assets/style.css's own defaults, independent of any preset:theme: accent: "#4cc9f0"
Overridable keys:
bg,bg-panel,fg,fg-muted,accent,accent-dim,border, andtitle-fg— the title slide'sh1/subtitle/author-date text color, which defaults tofgand only needs setting iffgdoesn't contrast against the title slide's translucent dark panel (a fixed color, independent of the theme) — e.g. a light-background theme with a darkfg, likefhiabove, which sets it to white.
CLI
deckoction <input.md> # build into build/ next to input, then serve it
[--force] # overwrite a non-empty output directory (needed to rebuild)
[-o DIR] # output directory instead of build/ next to input
[--port N] # default: 8000
If a preview server from an earlier run is still on the port, it's stopped first; any
other process on the port is an error (use --port).
This quick preview shortcut is also available as slides_md <input.md> (identical
options). For every other option use the build subcommand:
python -m deckoction build <input.md> -o <output_dir>
[--config deck.yaml] # default: deck.yaml next to input, if present
[--images-dir DIR] # default: input file's directory
[--strict] # promote warnings (missing image_alt, unknown fields, etc.) to errors
[--force] # overwrite a non-empty output directory
[--serve] # serve the output with `python3 -m http.server` after building
[--pdf PATH] # also export a PDF (one page per slide) to PATH, via headless Chromium
[--thumbnail PATH] # also export a PNG screenshot of the title slide to PATH
Only images actually referenced by a slide are copied into <output>/slide_images/.
A missing referenced image is always a build error, --strict or not. A .pdf file
can be used anywhere a PNG/JPEG can — its first page is rasterized (200 DPI) and
treated identically from there on — with the pdf-images extra (uv sync --extra pdf-images).
--pdf and --thumbnail both require the pdf extra (uv sync --extra pdf) plus
a one-time uv run playwright install chromium to download the browser.
Development
uv sync
uv run pytest
uv run ruff check .
CI (.github/workflows/ci.yml) runs tests (Python 3.10-3.12), ruff check, and a docs
build with warnings promoted to errors on every push/PR — see docs/contributing.md for
details.
examples/demo/slides.md exercises all six layouts, background alpha, citations, and
KaTeX — useful as both a smoke test and a syntax reference.
docs/_static/cheatsheet.pdf (the downloadable cheatsheet linked from the docs site) is
generated from examples/cheatsheet/slides.md by scripts/build_cheatsheet.py — a
dev-only script needing the pdf extra, same as --pdf/--thumbnail. Re-run it and
commit the result whenever that source deck changes:
python scripts/build_cheatsheet.py
Metadata
Release files for Deckoction-md 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deckoction_md-0.3.0.tar.gz | 3.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deckoction_md-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 5.0 MB
Release files / deckoction_md-0.3.0.tar.gz
| Download URL | deckoction_md-0.3.0.tar.gz |
|---|---|
| Size | 3.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aa4c641070fb8170078653a3c5e3e2bfadff1f477504b9eede616195fac0680a
|
|
BLAKE2b-256 checksum How to use checksums |
cb6bace6901527a06da5997bdfa022345b1116c2e85586de1594903693ef8edf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / deckoction_md-0.3.0-py3-none-any.whl
| Download URL | deckoction_md-0.3.0-py3-none-any.whl |
|---|---|
| Size | 1.5 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54ea02eef92e5b405e112de477363abb65ae34f1d2075143111fb388fa96114e
|
|
BLAKE2b-256 checksum How to use checksums |
d64eb4ed68b70e786280436dbcc3dc3d7214a92e6e8f184170bf5e23a8336fdd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|