Skip to main content

deckoction

CI Documentation Status

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_level included). The formula box 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 through phase_images full-bleed, one per step, in place of the single image field. There's no extra final step: 3 images → 3 steps. title/kicker stay 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
    default The original dark/gold theme (used automatically if theme: is omitted — no visual change either way)
    alomancy Dark, 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 slide
    neuefische Dark violet background with Neuefische's brand orange (#f44717) as the accent, the "Poppins" font, and a subtle Neuefische logo watermark
    fhi Light 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 overrides title-fg (see below) to stay legible

    New presets are added by registering a ThemeDefinition in deckoction/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, and title-fg — the title slide's h1/subtitle/author-date text color, which defaults to fg and only needs setting if fg doesn'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 dark fg, like fhi above, 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)

Source distribution for Deckoction-md 0.3.0
File Size Uploaded
deckoction_md-0.3.0.tar.gz 3.6 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for Deckoction-md 0.3.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page