Tutorial · feature test · live demo
Every feature is demonstrated by using it. Two-column slides show the Markdown on the left and what it produces on the right.
Try it lines are things to do right now, on this deck. If one doesn't work, that's a bug: this deck is MD-Slides' tutorial and its acceptance test.
Slide separators (---) can't appear as bare lines inside code blocks, so the examples show slide bodies only. Open feature-tour.md for the full format.
Eight sections: Templates · Content · Speaker view · Navigation · Any screen · CLI · Themes · Validation
Six slide types — each with named slots and enforced constraints
Three slots: H1 title (required, ≤ 2 lines), H2 subtitle (optional, ≤ 2 lines), **Author** (optional, ≤ 80 chars).
# Main Title
## Optional subtitle
**Optional author**
title is the only template that uses H1. All other templates use H2. The first slide of this deck uses the title template.
The content template is the workhorse — most slides use it. Two slots:
## Slide Heading
Body: markdown, lists, code blocks, images, tables.
Heading: required, ≤ 80 chars. Body: ≤ 12 lines and ≤ 150 words. Validation reports all violations together — you fix everything in one pass, not one at a time.
section-title and closing templatesOpen chapters with section-title:
template: section-title
## Chapter Title
Subtitle text
Both parse like content. Themes assign each a distinct visual identity.
End the deck with closing:
template: closing
## Thank You
Questions welcome
Themes can apply full-bleed backgrounds to these templates automatically.
Split a slide into two independent columns using the ---column--- delimiter:
template: two-column
## Heading
Left column content here.
---column---
Right column content here.
Each column: ≤ 10 lines, ≤ 75 words. The ---column--- delimiter must be on its own line. The next slide demonstrates a live two-column layout.
two-column in action: before and afterSequential — simple but slow:
def processAll(items: List[Item]): Unit =
items.foreach(item => process(item))
Each item processed one at a time. Fine for small lists; bottleneck at scale.
Parallel — Cats Effect:
def processAll(items: List[Item]): IO[Unit] =
items.parTraverse(item => process(item))
parTraverse runs all items concurrently using the IO thread pool.
The diagram template renders Mermaid charts to SVG at build time — requires mmdc installed.
template: diagram
caption: Optional caption
## Slide heading
```mermaid
graph LR
A[Input] --> B[Output]
```
Flowcharts, sequence, class, Gantt, pie, ER, and state diagrams are supported. The next slide is a live diagram slide.
The MD-Slides render pipeline
Every slide opens with a frontmatter block between --- markers. Supported keys:
| Key | Required | Description |
|---|---|---|
template: | yes | Slide type: title content section-title two-column diagram closing |
header: | no | Top bar text — tokens: {{pageNumber}} {{totalPages}} {{timer}} {{title}} {{date}} |
footer: | no | Bottom bar text — same tokens as header: |
vertical-align: | no | Content position: top center (default) bottom |
background: | no | Per-slide background image path (overrides theme for this slide only) |
caption: | no | Caption below diagram (diagram template only) |
Formatting, lists, code blocks, images, and tables
Inline formatting: you write / you get
**Bold** text
*Italic* text
`Inline code`
[Link text](https://github.com/TJMSolns/MD-Slides)
~~Strikethrough~~
Bold text Italic text Inline code Link text Strikethrough
Lists: you write / you get
- First item
- Second item
- Nested level 2
- Nested level 3
1. Ordered first
2. Ordered second
- Mixed nesting
Fenced code blocks with a language name get full syntax highlighting:
case class Slide(id: SlideId, template: Template, slots: Map[SlotName, SlotContent])
object SlideDeck:
def validated(slides: List[Slide]): Either[NonEmptyList[ValidationError], SlideDeck] =
Either.cond(
slides.nonEmpty,
SlideDeck(slides),
NonEmptyList.one(ValidationError("Empty deck"))
)
190+ languages via highlight.js. Specify the language immediately after the opening fence: ```scala .
Code blocks: Python and Bash
def contrast_ratio(fg, bg):
l1 = luminance(fg)
l2 = luminance(bg)
brighter = max(l1, l2)
darker = min(l1, l2)
return (brighter + 0.05) / (darker + 0.05)
# WCAG AA: 4.5:1 for normal text
assert contrast_ratio('#000','#fff') >= 4.5
# Download the JAR
curl -L https://github.com/TJMSolns/\
MD-Slides/releases/latest/\
download/md-slides.jar \
-o md-slides.jar
# Render with dark theme
java -jar md-slides.jar \
render my-talk --theme dark
def spell(n: Int): String = n match
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case 3 => "three"
case 4 => "four"
case 5 => "five"
case 6 => "six"
case 7 => "seven"
case 8 => "eight"
case 9 => "nine"
case 10 => "ten"
case 11 => "eleven"
case 12 => "twelve"
case 13 => "thirteen"
case 14 => "fourteen"
case 15 => "fifteen"
case 16 => "sixteen"
case 17 => "seventeen"
case 18 => "eighteen"
case 19 => "nineteen"
case 20 => "twenty"
case _ => n.toString
Code blocks over 20 lines shrink to fit, and don't count against the 12-line body limit.
Images: you write / you get

Path is relative to the .md source file. MD-Slides copies images to the output directory automatically.
Alt text is required — missing alt text is a WCAG 2.1 validation error.
For self-contained files: 
Tables: you write / you get
| Template | Max body | Used for |
|----------|----------|----------|
| `title` | — | Opening slide |
| `content` | 12 ln / 150 w | **Most** slides |
| `two-column` | 10 ln / 75 w | Comparisons |
| `diagram` | — | Mermaid charts |
| `closing` | 12 ln / 150 w | Final slide |
Column alignment: |:---:| center · |---:| right. Bold, code and links work inside cells.
| Template | Max body | Used for |
|---|---|---|
title | — | Opening slide |
content | 12 ln / 150 w | Most slides |
two-column | 10 ln / 75 w | Comparisons |
diagram | — | Mermaid charts |
closing | 12 ln / 150 w | Final slide |
The diagonal stripes behind this slide come from its frontmatter. In your own deck you'd write:
template: content
background: images/stripes.png
background: takes an image path (copied into the output) or a data: URL, as here, which keeps the tour a single file. It overrides the theme background for this slide only. PNG, JPEG, SVG and WebP all work.
Notes, next-slide preview, and elapsed timer
Speaker notes: you write / you get
<!-- Speaker notes: Key point here.
Don't forget to mention Y.
Notes can span multiple lines. -->
Place the comment anywhere in the slide body. Press S to open speaker view.
In speaker view, you see:
Notes are never visible in the main window.
The speaker window shows the slide counter with ‹ › buttons, the elapsed timer, your notes, and the next slide's heading. On a phone it becomes one column.
The two windows stay in step: move in either one and the other follows (same browser, same device).
Timer: T pauses or resumes, R resets. {{timer}} in a header or footer shows it on slides.
Try it: press S, then change slides in either window.
Every keyboard shortcut
| Key | Action |
|---|---|
→ ↓ Space Enter PgDn | Next slide |
← ↑ PgUp | Previous slide |
Home / End | First / last slide |
F / S | Fullscreen / speaker view |
B / L | Break mode / save session log |
G | Goto: type slide number, press Enter |
P / N | History back / forward |
T / R | Pause / reset timer |
Clickers just work. On a phone or tablet, swipe, tap, or use the ‹ › buttons.
Break mode (B): hides the slides and pauses the timer. Press B again to resume. Set a custom image with --break-screen or breakScreen in your config.
Goto (G): type a slide number and press Enter, or pick a slide from the list. On a touch screen, tap the slide counter.
History (P / N): back and forward through the slides you actually visited.
Try it: press G, type 3, press Enter, then press P to come back here.
Laptop, projector, phone or tablet
Touch, clickers and fullscreenTouch screens:
Clickers send Page Down / Page Up or arrow keys, so they just work.
Fullscreen: press F or the ⛶ button (hidden where the browser can't do it, as on iPhone).
Try it: swipe to the next slide, then come back with a tap on the left.
| Stage | Input | Parser | Validator | Renderer | Theme | Assets | Output |
|---|---|---|---|---|---|---|---|
| 1 | deck.md | Flexmark | Structure | Scalatags | JSON | Images | index.html |
| 2 | Frontmatter | Slots | Density | Speaker view | Fonts | Diagrams | speaker.html |
| 3 | Notes | Templates | WCAG 2.1 AA | Navigation JS | Colours | Break image | Session log |
This table is wider than a phone screen. The slide shrinks to fit, and the table re-wraps rather than overflowing.
Try it: rotate your phone, or resize this window. The slide re-fits.
The address bar tracks the current slide as #N. Reloading keeps your place, and a link can open any slide:
https://www.tjm.solutions/mdslides/demo/#12
Changing slides doesn't add browser history, so Back (or a phone's back gesture) leaves the deck rather than stepping through it. Use P and N for slide history.
Try it: look at the address bar, then reload. You stay on this slide.
render, display, report, config, and smart default
render: markdown to HTML
java -jar md-slides.jar render my-talk
java -jar md-slides.jar render my-talk \
--theme dark
my-talk, my-talk.md and talks/my-talk all resolve. Errors (structure, missing alt text) stop the render; density limits only warn.
my-talk/
index.html ← presentation
speaker.html ← speaker view
sync.js ← plus 7 small
… support scripts
images/ ← copied assets
| Flag | Default | Purpose |
|---|---|---|
--theme NAME | light | light, dark, corporate, or a theme folder in ./themes |
--no-copy-images | copy on | Skip copying image assets |
--skip-accessibility | check on | Skip WCAG 2.1 AA validation |
--accessibility-report FILE | off | Write validation results to JSON |
--break-screen IMAGE | none | Image shown in break mode |
-i FILE / -o DIR | — | Explicit input and output paths |
Custom themes are folders: themes/mytheme/theme.json, used as --theme mytheme.
java -jar md-slides.jar config
Global Configuration:
Location: ~/.mdslides/config.json
Settings:
defaults.theme: light
Project Configuration:
Location: .mdslides/config.json
Settings:
theme: dark
Shows which settings are in effect and which file each came from.
Session log: how did the talk go?While presenting, MD-Slides records every slide change (and how it happened), timer pauses and breaks.
Press L to save the log. Your browser saves it as my-talk-<date>-<time>.log.json.
Try it: press L now.
java -jar md-slides.jar report my-talk
Report shows:
It finds the newest log in Downloads, or use --log FILE.
java -jar md-slides.jar my-talk
my-talk.md changed since the last renderdisplay my-talk just opens it; render my-talk just renders.
MCP server: decks from an AI assistantmdslides-mcp.jar (on the releases page) runs MD-Slides as a Model Context Protocol server over stdio.
java -jar mdslides-mcp.jar
| Tool | Does |
|---|---|
render_deck | Render a deck |
validate_deck | Check a deck |
list_themes | List themes |
get_deck_info | Count slides, templates, images |
Distributing your presentationThe output directory is self-contained:
# Web server
scp -r my-talk/ user@server:/var/www/html/
# GitHub Pages
# Push my-talk/ to docs/ or gh-pages branch
# Email: zip the folder
zip -r my-talk.zip my-talk/
index.html opens in any browser — no server required.
PDF export — browser print (speaker notes stay out of the PDF):
my-talk/index.htmlOr with headless Chrome:
chromium --headless \
--print-to-pdf=my-talk.pdf \
my-talk/index.html
Fullscreen and browser tipsFullscreen: F, the ⛶ button, or F11 (Windows/Linux) / Ctrl+Cmd+F (Mac).
Syntax highlighting loads from a CDN, so code is highlighted when you're online. Everything else works offline.
Opening speaker view:
If speaker view won't open, allow pop-ups for the page, or serve the folder:
python3 -m http.server 8080
Built-in themes, custom JSON themes, and four-layer config
java -jar md-slides.jar render my-talk --theme light # default
java -jar md-slides.jar render my-talk --theme dark
light — white background, dark text. dark — dark background, light text, teal accents. corporate — a neutral business look.
All pass WCAG 2.1 AA contrast. This deck's live demo uses dark.
In themes/mytheme/theme.json:
"background": { "color": "#1e1e1e", "image": "images/deck-bg.jpg" },
"templateBackgrounds": { "section-title": "images/section.png" }
Theme image paths are relative to the theme folder, and are copied into your output.
Priority: per-slide background: > templateBackgrounds > deck background.
Custom themes: a theme folder
themes/
mytheme/
theme.json
images/
java -jar md-slides.jar render \
my-talk --theme mytheme
theme.json needs every section: name, version, background, colors, fonts, spacing, syntax, slideCounter.
Copy a complete example from the docs' Themes page and change what you need.
Per-template configuration in themes
"templateConfig": {
"section-title": {
"header": "{{pageNumber}}/{{totalPages}}",
"verticalAlign": "top"
},
"closing": {
"footer": "{{title}} · {{date}}"
}
}
| Key | Effect |
|---|---|
header | Header text, with tokens |
footer | Footer text, with tokens |
verticalAlign | top center bottom |
colors | Colour overrides |
Every slide of that template gets it, with no frontmatter needed.
This slide's frontmatter:
header: Feature Tour — Slide {{pageNumber}} of {{totalPages}}
footer: {{title}} · {{date}} · {{timer}}
Tokens: {{pageNumber}} {{totalPages}} {{timer}} {{title}} (the deck title) {{date}} (today).
Try it: check the header and footer on this slide. The timer ticks.
Content position per slide via frontmatter:
template: content
vertical-align: top
Three options: top · center (default) · bottom
Useful for sparse slides where centered text looks visually unbalanced, or for slides with code blocks that benefit from a predictable top position.
Highest wins: CLI flags > project .mdslides/config.json > global ~/.mdslides/config.json > defaults
{ "theme": "dark", "outputDir": "dist", "breakScreen": "images/break.png" }
{ "defaults": { "theme": "light" }, "paths": { "themesDir": "~/themes" } }
The first is a project config; the second a global one, where settings go under defaults.
All errors collected and shown together
Errors (the render stops): missing required slots, unknown templates, a two-column slide without exactly one ---column---, images without alt text, image files that don't exist.
Warnings (the render continues): headings over 80 characters, bodies over 12 lines or 150 words, columns over 10 lines or 75 words, theme colours below WCAG 2.1 AA contrast.
✗ Validation failed with 4 problem(s):
- Slide 3: Structure Error - Required slot 'heading' is missing
- Slide 3: Structure Error - Required slot 'body' is missing
- Slide 4: Unknown template 'bogus'. Valid templates: title, content, diagram, closing, section-title, two-column
- Slide 5: image file 'images/lamp.png' does not exist (looked for images/lamp.png)
✓ Validation passed with 1 warning(s):
- Slide 2: Density Warning in 'body' slot - Content exceeds
recommended line limit (actual: 17, guidance: 12 lines)
Validation reports every problem on every slide in one pass, before anything is written, so you fix them all at once. An image without alt text is reported by name: .
Markdown → self-contained HTML presentations
Open the source. examples/feature-tour.md produced every slide in this deck.