Turn one Markdown file into a self-contained HTML presentation — charts, stat tiles, tables, cards and callouts included. No dependencies, no build chain, no JS in the output. Charts are rendered to inline SVG at build time, so the result is a single file that works offline, adapts to dark/light mode, and prints one slide per page.
It generalises a hand-rolled presentation design system into a reusable generator, so the next deck takes minutes, not hours.
crossPresent is a single, dependency-free Python file. Install it as a
crosspresent command:
pipx install . # isolated CLI (recommended) — needs pipx
# or: uv tool install .
# or: pip install . # into the current environmentRequires Python 3.8+ and nothing else. Prefer not to install? Every command
below also works as python3 crosspresent.py … straight from the repo.
crosspresent new mydeck # scaffold a starter deck
crosspresent build mydeck.md --openThat's it.
While writing, use live preview instead of rebuilding by hand:
crosspresent serve mydeck.md # opens a browser; save = reloadWant to see everything it can do first? This builds a bundled showcase deck and opens it — no files needed, works from any directory:
crosspresent demo(From a clone of the repo you can also build the source directly:
crosspresent build examples/demo.md --open.)
Three built-in looks. Pick one in frontmatter (template: deck) or override
at build time (-t deck):
| Template | What you get | Best for |
|---|---|---|
sidebar (default) |
Sticky table of contents + full-height scrolling sections | Walkthroughs, screen-shared reviews |
deck |
Full-screen slides with snap scrolling and a slide counter | Actual presentations |
doc |
Single column with a TOC box at the top | Documentation, reports, READMEs-with-pictures |
Same source file works with all three — examples/demo.html,
examples/demo-deck.html and examples/demo-doc.html are the same Markdown
built three ways.
---
title: My Deck # browser title (defaults to first slide)
subtitle: Shown under the cover heading.
eyebrow: Project walkthrough # small label above the cover heading
template: sidebar # sidebar | deck | doc
palette: default # default | ocean | forest | mono | crimson
accent: "#ffb547" # theme accent (optional, overrides palette)
background: "#0f1115" # page background (optional, overrides palette)
toc_title: Contents # sidebar heading (optional)
footer: My footer text # page footer (optional)
---A palette sets the accent, the page background and the chart colors as a
matched set, for both the dark and light schemes — pick one and everything
agrees without touching a hex code (theme: works as an alias):
| Palette | Feel |
|---|---|
default |
Amber accent on near-black — the original walkthrough look |
ocean |
Cyan/blue, deep blue-black background |
forest |
Greens and earth tones |
mono |
Grayscale charts, neutral accent — print-friendly, serious |
crimson |
Warm reds/pinks |
Explicit keys win over the palette: set palette: ocean and
accent: "#ff00aa" and you get ocean's backgrounds and chart colors with
your accent.
The output follows the system color scheme: a dark theme by default and a light theme when the viewer's OS prefers light. Both are themeable, and every key is optional — leave them out and you get the built-in look.
| Key | Default | Applies to |
|---|---|---|
accent |
#ffb547 |
dark scheme accent (eyebrows, notes, first chart color) |
accent_light |
#c2530a |
light scheme accent (defaults to accent if you set one) |
background |
#0f1115 |
dark scheme page background |
background_light |
#fbfbfd |
light scheme page background |
---
title: Branded deck
accent: "#7c5cff"
background: "#14101e" # any CSS color — or a gradient:
# background: "linear-gradient(160deg, #14101e 0%, #1a1430 100%)"
---Values are dropped into CSS as-is, so any valid CSS background value works (colors, gradients). Panels, cards and charts keep their own surfaces, so an override only needs to look right behind them — very dark values pair well with the dark accent palette, very light ones with the light palette.
# Title— a title slide (gradient background; first one is the cover)## Heading— a regular slide; auto-numbered, auto-added to the TOC## Heading {#custom-id}— set the anchor yourself### Heading— a sub-heading within the current slide- The first paragraph of each slide is styled as its intro line
**bold** *italic* `code` [link](https://…) 
[[BADGE]] → neutral pill badge
[[shipped|good]] → colored badge: good, warn, bad, info, accent, purple, teal, dim```chart
{
"type": "bar",
"title": "Requests per day",
"labels": ["Mon", "Tue", "Wed", "Thu", "Fri"],
"values": [120, 200, 150, 310, 260]
}
```| Key | Meaning |
|---|---|
type |
bar, hbar, line, area, pie, donut |
labels |
x-axis / slice labels |
values |
single series of numbers (≥ 0) |
series |
instead of values: [{"name": "a", "values": […]}, …] — gives grouped bars / multi-line + legend (bar, line, area; hbar, pie and donut are single-series) |
data |
instead of inline numbers: path to a .csv or .json file, relative to the deck (see below) |
title |
optional caption above the chart |
height |
optional SVG height (bar/line/area; default 280, minimum 80) |
colorful |
hbar only: cycle the palette per bar |
```chart
{ "type": "bar", "title": "Outcomes by week", "data": "data/weekly.csv" }
```CSV format: a header row, then one row per label — first column is the labels, every other column becomes a series (one column → a single series, several → grouped bars / multi-line with a legend):
week,passed,failed
W1,58,6
W2,60,4A .json data file is an object with labels and values (or series).
Regenerate the file in CI and rebuild — the deck stays in sync with real
output. serve watches *.csv / *.json next to the deck (and in
data/) and reloads when they change. Don't mix data with inline
labels/values/series — that's an error.
Charts become inline SVG using the theme's colors — they follow dark/light mode and print correctly. Worth knowing:
- Negative values aren't supported.
- Large numbers are abbreviated on axes and value labels:
1.2B,3.5M,150k. hbarrow labels longer than ~30 characters are truncated with an ellipsis — keep them short or use a table instead.- A bad spec (non-numeric value, unknown type, too-small height) fails the build with an error naming the slide; a label/value count mismatch builds but prints a warning.
```stats
63 | passed | green ← value | label | color (green/amber/red/blue/gray)
```
```note Optional label
A highlighted callout box.
```
> **Or as a blockquote**
> First bold line becomes the note label.
```cards
### Card one
Splits on ### into a grid. Force columns with ```cards 3
### Card two
…
```
```pills
Link text | https://example.com ← row of link chips
```
```python
any other fence tag = code block
```
```mermaid
graph TD; A-->B
```The one exception to "no JS": if a deck contains a mermaid block, the
output includes a CDN <script> so diagrams render — that page then needs
network access. Everything else is fully offline.
```notes
What to *say* on this slide — not what to show.
```Notes are stripped from the output entirely by default. Build (or serve)
with --notes and they render as dashed, clearly-marked boxes — so you can
keep one source file and produce both a rehearsal copy and a clean
shareable one. Add a label after the tag: ```notes Timing.
crosspresent demo build the bundled showcase deck and open it
-t, --template sidebar|deck|doc preview the demo in another template
--notes include the demo's speaker notes
--no-open build but don't open the browser
crosspresent new <name> [--force] scaffold a starter deck
crosspresent build <src.md> [<src.md> ...] build each source to <src>.html
-o, --output <path> choose output path (single source only)
-d, --output-dir <dir> write every deck's .html into <dir>
-t, --template sidebar|deck|doc override the template
--embed inline local images as data URIs
--notes include speaker notes
--open open each result in a browser
crosspresent serve <src.md> live preview: save = auto-reload
-t, --template sidebar|deck|doc override the template
--port <n> port (default 7350)
--notes include speaker notes
--no-open don't open the browser
build accepts several sources or a glob — crosspresent build decks/*.md
builds each next to its source, or all into one folder with --output-dir.
A deck that fails (e.g. a bad chart) is reported but doesn't stop the rest;
the command exits non-zero if any deck failed.
serve is a dev-mode preview: it builds in memory (nothing written to
disk) and injects a tiny reload script — the only time crossPresent output
contains JS you didn't ask for. build output is untouched.
| File | What it shows |
|---|---|
| examples/demo.md | Every block type, one slide each |
Open the HTML in a browser and print — the print stylesheet hides the TOC
and breaks one slide per page. (For the deck template, printing flows
slides naturally instead of snap-scrolling.)
python3 -m unittest discover tests