Skip to main content

decktile

Markdown to styled HTML slides with PDF and ODP output.

Why

Building a slide deck in PowerPoint is tedious. Writing markdown is fast. Decktile turns markdown into slides. Content lives in markdown files. Styling lives in CSS. No .pptx editing, no inline HTML, no copy-paste hell.

Other tools (Marp, Slidev) mix HTML and CSS into the markdown. This breaks separation of concerns - your slides become <div> soup that's hard to change later. Decktile keeps them apart: directives (@kicker, @layout, ::: card) are semantic; all styling is in theme CSS files.

Decktile is opinionated. It ships with card layouts, accent-colored quotes, ▸ bullet markers, dark mode. You theme it via CSS variables. If you want pixel-level custom layouts, use raw HTML. If you want a deck that looks good out of the box with minimal markup, this is the one.

Frontmatter

---
title: My Talk
theme: dynamia           # theme name (default, dynamia, kubecon_japan)
variant: dark            # global dark mode
logo: assets/logo.svg    # top-right logo (replaces theme default)
logo_dark: assets/logo-white.png  # dark mode variant of logo
watermark: assets/brand-mark.svg  # bottom-right watermark (conference logo); none = off
background: none                  # slide background image (replaces theme default); none = off
style: |                 # raw CSS override
  :root { --color-accent: #e91e63; }
---

Install

pip install decktile    # or: uv tool install decktile

HTML output works everywhere. PDF and ODP output use WeasyPrint, which needs Pango from your system:

  • Linux: usually present; otherwise apt install libpango-1.0-0 libpangoft2-1.0-0
  • macOS: brew install pango
  • Windows: install MSYS2, run pacman -S mingw-w64-ucrt-x86_64-pango, then set WEASYPRINT_DLL_DIRECTORIES=C:\msys64\ucrt64\bin

See WeasyPrint's install guide for details. From a checkout:

pdm install          # core + HTML/PDF/ODP
pdm install -G plot  # + seaborn/matplotlib for inline charts

Usage

pdm run decktile slides.md              # HTML + presenter view
pdm run decktile slides.md --odp        # + ODP (programmatic)
pdm run decktile slides.md --image-odp  # + ODP (screenshots from PDF)
pdm run decktile slides.md --pdf        # + PDF (images compressed)
pdm run decktile slides.md --pdf --no-optimize-images  # + PDF, full-resolution images
pdm run decktile -w slides.md           # watch and rebuild on changes
pdm run decktile --odp -w slides.md     # watch + ODP
pdm run decktile --dist slides.md       # HTML + all referenced assets as zip
pdm run decktile a.md b.md --pdf       # several decks
pdm run decktile '**/*.md' --pdf       # quoted globs are expanded (recursive with **)

A missing or failing file is reported (error: <file>: <reason>) and skipped; the rest still build, and the exit code is 1 if any file failed.

Assets

Image paths in markdown are relative to the source file. On build, decktile symlinks all directories from the source folder into dist/. Files and .md files are skipped, only directories are linked.

pdm run decktile slides.md    # symlinks slides/assets/ → slides/dist/assets/

--odp is the production-ready ODP path: a native ODF renderer (styles.py, geometry.py, shapes.py) that reproduces the HTML layout, colors, fonts, and positioning directly in content.xml/styles.xml - no PDF roundtrip. Cards, grids, tables, lists, quotes, speaker notes (from <!-- comment --> blocks), and lucide icons (rendered as colored glyphs from an embedded icon font) all map to real ODF shapes and styles, editable in LibreOffice Impress.

--image-odp remains available as a pixel-perfect fallback (PDF screenshot per slide embedded as an image) for decks the native renderer doesn't handle well yet.

If you know the ODF spec or odfdo internals, contributions to the native ODP renderer are welcome. See src/decktile/render/odp.py and src/decktile/render/odf/.

An Obsidian plugin that renders .md slides to HTML in-pane would be great. The Python CLI makes distribution tricky; if you've tackled that problem before, reach out.

Most of this codebase is AI-generated. I've kept it DRY where I could, but there's still duplication - the lucide icon rendering appears in a couple places that should be one. PRs that consolidate repeated logic are welcome.

Viewer controls

Action Key / Mouse
Next slide Left click (on slide area), Right arrow, Down arrow, PgDn, Space
Previous slide Right click, Left arrow, Up arrow, PgUp, Backspace
First slide Home
Last slide End
Toggle fullscreen f
Open presenter view Presenter button, p
Close presenter q

Slide directives

@kicker text           # title slide eyebrow
@subtitle text         # title slide subtitle
@speaker name=X role=Y<br>       github=Z twitter=Z email=Z # title slide attribution with social links
@layout name           # apply a slide layout
@col                   # explicit column break in two-col / compare layouts
@row                   # horizontal row within a column (side by side)
@tiny text             # small annotation below content
@variant dark          # switch to dark mode for this slide
@hidden                # exclude slide from output (alias: @hide)
{icon:star}            # inline lucide icon, e.g. {icon:heart stroke=#d05a39}

Layouts

Layout Usage
@layout two-col Heading full-width, content split 50/50. Use @col for explicit break.
@layout image-right Heading full-width, text left, image right
@layout image-left Heading full-width, image left, text right
@layout compare Two cards side-by-side with an arrow connector, conclusion notes below
@layout metrics-N 2-4 metric cards, auto-detected from ::: card {metric} blocks
@layout ecosystem Compact stacked layout for partner/device logos. Small labels, uniform image sizing
@layout timeline-vertical Cards alternate left/right of a central vertical spine, connected by stubs
@layout timeline-horizontal Cards alternate above/below a central horizontal spine
Custom @layout <name> adds CSS class layout-<name>, style via frontmatter style: block

Compare layout

@layout compare

## Before & After

::: card{ tag="red" }
### Without HAMi

GPU utilization at 65%, manual bin-packing required.
:::

::: arrow

:::

::: card{ tag="green" }
### With HAMi

GPU utilization at 92%, zero manual intervention.
:::

::: notes{ tag="green" }
> HAMi is the only CNCF project providing hardware-level GPU sharing.
:::

Metrics layout

Key metrics display 2-4 cards with a large number and supporting label. Auto-detected from consecutive ::: card {metric} blocks. No explicit @layout needed.

## Key Metrics

::: card {metric}
10x
Operational cost improvement
:::

::: card {metric}
50%
GPU utilization
:::

::: card {metric}
10x
Density improvement
:::

Each ::: card {metric} block: first line is the big number, following lines are the label. Styled with accent-colored large text (2.8em, bold) and dimmed label text. Grid auto-centers vertically in the slide body.

Timeline layouts

Cards alternate sides of a central spine -- left/right for timeline-vertical, above/below for timeline-horizontal. The spine spans the full available height/width; justify-content: space-evenly auto-spaces however many milestones there are along it -- no per-count tuning needed for a reasonable number of items. ::: card {focus} scales an item up and highlights it. ::: card {label="Q1"} prints a short name directly on the spine next to that card's own dot, separate from the card's fuller ### header. No explicit cols= needed beyond matching the layout's flow (cols=1 for vertical, one row for horizontal).

No automatic collision avoidance beyond that. space-evenly only distributes leftover space -- once total card height (vertical) or width (horizontal) exceeds the spine's fixed length, flexbox has no fallback to shrink text, and cards start genuinely overlapping. Keep it to 5-6 items per timeline slide; past that, split into multiple slides.

@layout timeline-vertical

## Project Roadmap

::: grid {cols=1}
::: card
### Q1 2026
Kickoff and planning.
:::
::: card {focus}
### Q2 2026
Major public launch.
:::
::: card
### Q3 2026
Scale and iterate.
:::
::: card
### Q4 2026
Expand to new markets.
:::
:::
@layout timeline-horizontal

## Release Timeline

::: grid {cols=4}
::: card
### v1.0
Initial release
:::
::: card {focus}
### v2.0
Big feature launch
:::
::: card
### v2.1
Bugfixes
:::
::: card
### v3.0
Next major
:::
:::

timeline-horizontal cards have a fixed height (keep text brief -- a title plus one short line) so the connector stub reliably reaches the spine regardless of content length.

Section labels and side images

Ecosystem slides use #### (h4) for section labels above grids, and ::: card {side-image} for images beside content without affecting row sizing:

#### Ecosystem & Device Support

::: grid {cols=2}
::: card
![NVIDIA](devices/nvidia.png) ...
:::
::: card {side-image}
![GitHub QR](ecosystem/github-qr.png)
:::
:::

Compare layout

The arrow block accepts text or images:

::: arrow
⚠
:::

::: arrow
{icon:arrow-right size=24}
:::

Fenced blocks

::: grid {cols=2}              # responsive grid
::: grid {cols=3}              # 3-column grid
::: card                        # basic card
::: card{ tag="green" }         # colored left border + background
::: card{ tag="quote" }         # accent left border, italic, no fill
::: card {metric}                      # big number + label card (first line = value, rest = label)
::: card {blueprint}                   # blue engineering-drawing panel; a ```dot inside uses the blueprint palette
::: arrow                       # connector for compare layout
::: notes{ tag="green" }        # full-width conclusion card
> quote text                    # blockquote, renders as .quote div
| col1 | col2 |                 # pipe table
`inline code`                   # inline code
```language                    # fenced code block with syntax highlighting
```pdg                         # predictygraph diagram, inline SVG (recommended)
```mermaid                     # Mermaid diagram, inline SVG
```seaborn                     # Seaborn chart, inline SVG

Card/grid {...} attributes: bare words are CSS classes ({metric} → .card.metric), k=v pairs produce k-v classes ({tag=green} → .card.tag-green). Grid also accepts cols=N and class=name literally.

Lucide icons

Use {icon:star} anywhere inline - headings, paragraphs, tables, card headers, card body, arrows, notes. Powered by python-lucide.

{icon:star}                              # default 1em height
{icon:check cls=accent-primary}          # uses theme accent via CSS class
{icon:heart cls=accent-secondary size=1.2em}  # class + explicit size
{icon:arrow-right size=32}               # pixel size

Parameters: stroke, fill, width, height, size (sets both), cls (CSS classes).

Use cls=accent-primary, cls=accent-secondary, or cls=accent-contrast to match theme accent colors instead of hardcoded hex values.

No configuration needed - pdm install includes the dependency.

Diagrams (predictygraph)

```pdg blocks are drawn with predictygraph, installed with decktile. Nothing else to install. predictygraph is the recommended way to draw diagrams in decktile, ahead of mermaid: it takes the slide's font and tag colours, its layout is predictable and readable, and it can animate (see Animated diagrams below). Node classes match decktile tag colours:

```pdg
graph dir=LR
app "Application" .green
db "Database" .cyan shape=cylinder
app -> db
```

Available node classes: green, cyan, yellow, red. The diagram uses the slide's body font. Put a css { … } block inside the diagram to change anything. See predictygraph's spec for the full syntax.

Animated diagrams

In the HTML deck, diagrams can move. PDF and ODP get a clean still frame of the same diagram, so nothing extra is needed for print.

```pdg
graph dir=TB
user "User" .green border=pulse
gpu "GPU pool" .yellow shape=cylinder border=glow
user -> gpu flow=forward "request"
```
  • flow=forward, reverse, both or auto: dots travel along an edge. flow-speed=0.8s sets the pace.
  • border=pulse, march or glow: the outline of a node or group breathes, crawls or glows. border-speed= sets the pace.
  • Set them per edge or node, with default edge flow=forward, or in CSS: edge.hot { flow: forward; }.

Wrap a diagram in ::: card {blueprint} for an engineering-drawing look: blue grid card, light lines and text, translucent node fills, with the node classes as brighter accents. Works the same under any theme.

::: card {blueprint}
```pdg
graph dir=LR
pod "Pod" .red
sched "hami-scheduler" .cyan
pod -> sched
```
:::

Mermaid diagrams

```mermaid
graph LR
    User((User)) --> API[API Gateway]
    API --> DB[(Database)]
```

Renders inline SVG in HTML, PDF in ODP. Uses the mermaidx Python package.

For new diagrams, prefer ```pdg (predictygraph): it follows the slide theme and can animate. Mermaid is there for diagrams you already have.

Seaborn charts

```seaborn
tips = sns.load_dataset("tips")
sns.scatterplot(data=tips, x="total_bill", y="tip", hue="day")
```

Runs Python in-process, renders inline SVG. Requires pdm install -G plot. Pre-imported: sns, plt, pd, np. Set seaborn_theme: kcd_vietnam in frontmatter to apply brand colors from the matching CSS theme file. Colors and fonts are parsed from the :root block. Falls back to seaborn palette names (Paired is the default, also deep, muted, pastel, etc.) if no theme CSS matches.

Custom charts with matplotlib

For charts beyond seaborn's high-level API, drop into matplotlib directly. Theme colors are available through rcParams -- no hex codes needed:

rcParam Maps to
plt.rcParams["axes.facecolor"] Card background
plt.rcParams["axes.edgecolor"] Primary accent
plt.rcParams["text.color"] Foreground text
plt.rcParams["xtick.color"] Dimmed/muted text
"C0" … "C5" Theme palette (Paired-derived brand colors)

For a slide-style figure background with a clear plot area:

fig, ax = plt.subplots()
ax.set_facecolor("none")
fig.patch.set_facecolor(plt.rcParams["axes.facecolor"])

For semantic colors use plt.get_cmap("Paired") and sample by position:

cmap = plt.get_cmap("Paired")
danger = cmap(4.5 / 12)  # muted red
ok = cmap(2.5 / 12)      # green
neutral = cmap(0.5 / 12) # light blue

See examples/seaborn_demo.md for the memory oversubscription chart (horizontal stacked bars, limit markers, annotations).

Layout caveats

@col overrides auto-detection in all layouts. Use it when auto-split puts content in the wrong column.

Slide transitions

Set a default in frontmatter or override per-slide:

---
transition: fade
---
@transition slide
Transition Effect
fade Opacity 0 → 1
slide TranslateX + fade
zoom Scale .85 + fade
wipe Clip-path reveal

All transitions run 0.4s, forwards-only (incoming slide animates).

Dark mode

Set variant: dark in frontmatter for all slides, or @variant dark per slide:

---
title: My Talk
variant: dark
---
@variant dark

## This slide uses the dark theme

Per-slide overrides work as slideshow transitions: @variant light switches back to light mode on the next slide.

Theming

Colors, borders, and spacing use CSS custom properties. Override them via frontmatter style: block or a custom theme file. See THEMING.md for the full variable reference.

---
style: |
  :root {
    --color-accent: #e91e63;
  }
---

Speaker notes

Any HTML comment in a slide becomes speaker notes in the presenter view:

---

<!--
These are speaker notes.
They appear in the presenter view.
-->

Demo

examples/features_demo.md is a 17-slide deck exercising every feature: title slides, @layout two-col, @layout image-right, @layout compare, grids with tagged cards, tables, fenced code blocks, mermaid diagrams, seaborn charts, predictygraph diagrams, lucide icons, blockquotes, speaker notes, and all directives.

See also: examples/mermaid_demo.md, examples/seaborn_demo.md, examples/pdg_demo.md, examples/lucide_demo.md, examples/timeline_demo.md. examples/seaborn_demo.md ends with hand-drawn matplotlib explainer charts from a real talk.

Pipeline

slides.md
  → markdown-it-py (parse)
  → Document AST (headings, paragraphs, grids, cards, tables)
  → build_ir() (resolve theme styles via tinycss2)
  → SlideIR (font_size, color, accent, SVG/PDF, + rendered HTML)
     ↙              ↘            ↘
  html.py          odp.py      pdf.py
  (_render_elem)   (_render_elem)  (weasyprint)
                                    ↘
                              image_odp (pdftoppm → PNG → ODP)

The IR is the single source of truth between renderers. Each Elem carries pre-rendered inline HTML (for the browser) plus resolved style properties (font_size, color, accent, muted) for the ODP renderer. Seaborn and mermaid SVGs are generated at IR build time and shared across renderers.

Architecture decisions

Why Python over Go

Go has no ODP library. No weasyprint equivalent. Goldmark works but its plugin ecosystem is thin, and Go's compile cycle adds friction to design work where you rebuild after every 2px change.

Why Python over Rust

Rust comes closer than Go: pulldown-cmark handles markdown well, cssparser does proper CSS parsing. But no odfdo equivalent, no weasyprint. Syntect covers fewer languages than Pygments for syntax highlighting.

Why not Node

Node slide tools ship a dev server, a bundler, a hot-reload daemon, and usually Electron just to render markdown to <div> tags. Every dependency adds a maintenance burden and expands the security surface. Python needs weasyprint and odfdo. That's the stack.

The Python sweet spot

  • odfdo: ODP generation via OpenDocument XML
  • weasyprint: HTML/CSS to PDF via embedded layout engine, no browser dependency
  • markdown-it-py: same parser as the JS ecosystem, GFM support
  • Pygments: comprehensive syntax highlighting for code blocks
  • Jinja2: mature templating, CSS injection, template includes

Deployable as a single binary via PyInstaller or Nuitka when needed.

License

GPL-3.0-or-later. See LICENSE.

  • Marp - Markdown to slides, JS/Electron, extensive ecosystem
  • Slidev - Markdown to slides, Vue/Node, presenter mode, rich theming
  • reveal.js - HTML presentation framework, JS
  • landslide - Markdown to slides, Python, dormant
  • lookatme - Terminal markdown presentations, Python

Metadata

Release files for decktile 0.2.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for decktile 0.2.5
File Size Uploaded Provenance Actions
decktile-0.2.5.tar.gz 141.8 kB Without provenance Details for decktile-0.2.5.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for decktile 0.2.5
File Interpreter ABI Platform Actions
decktile-0.2.5-py3-none-any.whl Python 3 none any Details for decktile-0.2.5-py3-none-any.whl

Total release size: 273.0 kB

Release files / decktile-0.2.5.tar.gz

Download URL decktile-0.2.5.tar.gz
Size 141.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ef4291c028e8ca28af1064648e32ba3436e86c635a055e0ba3e6b8cb1dce0b71
BLAKE2b-256 checksum
How to use checksums
c4766e646f8a020a641f40ae82d8cc6b43db7a01f0a33e33b6de0bec48730969
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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":true}

Release files / decktile-0.2.5-py3-none-any.whl

Download URL decktile-0.2.5-py3-none-any.whl
Size 131.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ed9d9114d3b5c719dede391e5f87926bf31fdca7da586a9210b9e4596923bc0
BLAKE2b-256 checksum
How to use checksums
010249aefdc968285fcf89dad63403d6e353835ee4f9a07635cddd233a5241ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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":true}

Security for decktile 0.2.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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