md-to-scrolldeck
Turns one Markdown document into a single self-contained vertical scroll-snap deck — scrollytelling for the leader who will scroll it themselves or project it in a review.
Hard requirements
Nine rules the skill does not bend. Most exist because a specific failure
happened once and is documented in the skill's references/.
Never write the HTML from scratch
Copy scroll-deck-skeleton.md and replace its
YOUR ... HERE tokens. The chrome is load-bearing and the
checklist greps for it.
One Write call, no chat echo
The whole file goes out in a single Write. The generated
HTML is never echoed into chat — summary plus file:// URL
only. This is what prevents Bedrock's
Truncated event message received.
No menu, no theme toggle, no PNG export
A deliberate divergence from visuals:visualize. A scroll
deck is one designed light-paper theme; dark slides are editorial
contrast, not a user setting. Do not add .viz-menu back.
Never invent content
Every figure, name, and quote comes from the source document. A slide with no source line range is a slide that gets cut.
CriticalNo base64 font by default
The webfont comes from a CDN. Embedding it is a multi-megabyte single
line that trips the same Bedrock truncation.
--offline-font exists but warns first.
Not a horizontal carousel
No translateX slide transitions, no click-zone
navigation, no Reveal.js. Vertical scroll-snap only.
One file in, one file out
No multi-file batching, and the input Markdown is never modified. Existing output is overwritten silently but always reported.
Verify before reporting
Run the checklist greps against the file just written. Any failure is
fixed and rewritten first — never report a broken dot rail as
[OK]. Waived items are [WARN], never silent.
xdg-open, never wslview
xdg-open on Linux and WSL, open on macOS.
wslview errors on HTML files.
Core principles
The markup is the easy part. What makes a hand-made deck beat a generated one is that someone decided what the document is about, and cut the rest.
Editorial compression
Not a heading-to-slide map(). Sections merge, split, or
get cut. Headings mark where an author changed topic; slides mark
where a presenter changes what the audience should be thinking about.
Read the whole document first
You need the ending to know what the opening should set up. No outlining after the first screenful.
Fixed skeleton
Progress bar, phase header, dot rail, observer, print styles — all copied, never improvised. The checklist verifies each one is still wired.
Shape, not just sequence
Three to five structural phases — WHY to
WHAT to NOW to NEXT to
START — become data-phase values that the
header reads live.
Resilient by default
Content is visible without JavaScript, motion respects
prefers-reduced-motion, snap turns off under 767px, and
print gives every slide its own page.
Vary the treatment
No three consecutive slides look identical. The skeleton ships a variant rotation to rotate through.
Arguments and flags
Exactly one positional. Two or more input files is a hard stop —
[FAIL] one Markdown file per run.
- <input.md>Required. Path to the source Markdown file, one per run. A missing path fails; a non-
.mdextension only warns. - --out <path>Explicit output path. Default is the input's directory and basename with an
.htmlextension —docs/vision.mdbecomesdocs/vision.html. - --slides <n>Force a slide count instead of the curated estimate. Range 5 to 20.
- --outline-onlyPrint the nav-dot outline and stop. No HTML is written.
- --lang <code>Override
<html lang>, e.g.enorko. Inferred from the source when omitted. - --offline-fontEmbed the webfont as base64 instead of linking a CDN. Warned and discouraged.
- --no-openSkip the
xdg-open/openstep after writing.
Process
Three steps. Step 2 is where the quality is decided.
Parse arguments and validate the input
Resolve the output path, confirm the source is readable, and stop early on a missing file or a second positional.
Read the whole document, then outline
Compress the source into 5 to 20 narrative beats and print the
nav-dot outline with source line ranges and the cut list. Fewer than
5 beats warns that a visuals:visualize one-pager fits
better; more than 20 proposes a scope cut rather than silently
building 30 slides.
Build, write once, verify, deliver
Fill the skeleton, write the file in one call, run the checklist
greps against what was written, fix and rewrite on any failure, then
open it and report slides=<n> with the
file:// URL, the slide-id list, and what was cut.
Curation: merge, split, cut
Target roughly one slide per 30 to 40 source lines, then adjust by judgement. Treat it as a starting estimate, never as arithmetic.
Merge
- Consecutive headings are facets of one concept
- A section is a short list that fits as a card grid inside its parent's slide
- A section is only a bridge sentence
Split
- One heading contains two distinct claims
- A heading holds a long process the audience must follow step by step
- A table has more than about 5 rows and its rows are the actual content
Cut
- Reference lists and link dumps
- A caveat only the author cares about
- Anything repeating a point already made
| Source length | What that usually means | Typical deck |
|---|---|---|
| under 150 lines | Often a better one-pager than a deck | 5–8 slides |
| 150–400 lines | The comfortable range | 8–13 slides |
| 400–800 lines | Cutting starts to matter a lot | 12–18 slides |
| over 800 lines | Ask whether to split the document first | 15–20 slides |
Measured on the reference deck: 39 headings over 535 source lines became 15 slides. One section's four subsections collapsed into a single slide because they were four facets of one idea; the reference list at the end became zero slides. The ratio is not the point — the judgement is.
What every deck ships with
Load-bearing chrome from the skeleton. The pre-flight checklist greps for each of these by name.
01 / NN counter
Right-edge dot rail
scroll-snap-type: y mandatory
IntersectionObserver tracking
Arrow-key navigation
PageUp / PageDown / Home / End / Space
Per-slide scroll cues
Print-ready, one slide per page
prefers-reduced-motion
Visible without JavaScript
aria-labelledby per slide
Snap disabled under 767px
Korean word-break: keep-all
Pre-flight checklist
Five groups, verified by inspection of the written file rather than from memory. Most items are a single grep.
Curation
Outline shown before any HTML; 5 to 20 slides matching that outline; narrative beats rather than a heading copy; cuts reported; every slide traceable to source lines; headlines read as a story; one idea per slide; treatments alternate.
Scroll chrome
Snap type and align present; dot count equals slide count in the same
order; progress bar wired to updateProgress(); counter
reads the real total; observer sets the active slide; keys navigate;
every slide but the last has a cue pointing at the next one.
Accessibility
Reduced-motion media query disables animation; content visible without
JS; every slide has aria-labelledby matching its own
heading id; <html lang> matches the source; no
horizontal overflow at 375px.
Snap turned off; progress bar, header, dot rail and cues hidden; every
slide breaking onto its own page; reveal animations forced visible;
dark slides setting print-color-adjust: exact.
Out of scope
Fails if present: a .viz-menu hamburger, a theme toggle or
.theme-dark class, PNG export or the
html-to-image CDN script, or a base64
@font-face the user did not ask for.
The common bug
Getting the cue chain off by one. Dot hrefs resolving to real slide ids, and each cue pointing at the next slide rather than any slide, are worth a throwaway script when the deck is long.
Related skills
The boundary is the deliverable, never the topic.
visuals:visualize
The general-purpose sibling for dashboards, infographics, posters, and horizontal decks. Use it when the output is not a vertical scroll deck — including when the source is too short to be one.
visuals:excalidraw-diagram
When the deliverable is a diagram rather than a deck — an
.excalidraw file rendered to PNG and audited against a
27-item checklist.