---
title: "sipario - a deck engine, taken out of a talk"
url: "https://www.pardel.dev/2026/09/04/sipario-a-deck-engine-taken-out-of-a-talk"
date: 2026-09-04
author: "Paul Ardeleanu"
tags: ["JavaScript","Node","Open-Source","Presentations","Public-Speaking"]
description: "I wrote a deck engine for one conference talk. Pulling it out into a library is what finally showed me which half was the engine."
---

# sipario - a deck engine, taken out of a talk

> TLDR; sipario is the engine behind one of my talks, pulled out as a standalone library. A talk is a folder: `deck.md`, its own templates, its own CSS. sipario supplies the format, the renderer and its checks, the presenter window and a server that reloads as you save. It ships no design of its own. No dependencies, MIT, `npm install sipario`.

<img src="/assets/images/blog/2026-09-04-sipario-a-deck-engine-taken-out-of-a-talk/thumb.png"
  width="180"
  alt="The sipario mark: a red theatre curtain hung from a scalloped valance trimmed in gold, drawn back on a dark stage holding one blank slide."
  title="The curtain up, and nothing on the stage yet. The design is the talk's.">

I wrote a deck engine because I wanted a conference talk to be one markdown file. A `# ` line opens a movement, a `## 3.2 A title` line opens a slide, `--` starts the next step of a build, and a fenced `notes` block is what I say over it. The server renders the file on every request, so nothing on screen can be a stale copy of something I've since edited.

Writing it for exactly one talk is why it worked. Taking it out is what showed me which half was the engine.

## What the extraction asked

Three questions, and none of them were visible while the engine and the talk lived in one repo.

**Where does a library's talk come from?** It doesn't. `render(src)` used to render whatever sat in `./talk`, via a module-level default. That's the worst shape a failure can take: it doesn't crash, it *succeeds*, against somebody else's slides. Now the template and image roots are required, and a missing one throws an error naming which.

**Who is this deck?** One talk's title was a string inside `server.js`. Its name was the `sessionStorage` key in `deck.js`. The same string again was the `BroadcastChannel` the deck and the presenter window talk over, in two more files. Three copies of one talk's identity, sitting in code every other talk would read. Now a deck opens with `name:` and says who it is — required, not defaulted, because a default is exactly how two decks come to be called the same thing, and two decks served from one origin sharing a channel means one deck's arrow key moves the other.

**Does the engine know any template by name?** It did, in one line: if the template was called `section` and had no word, use the movement's name. One line, and it meant the engine knew something about a template only my talk had. Now the movement's name reaches every slide as `meta.group` and the talk's own template is the thing that says a template called `section` is named by the movement it opens. The same rule caught the presenter window hunting for a slide's heading with a selector that named three more of my templates.

## The checks were the real prize

The old suite was 1,952 lines over a repo holding both an engine and a talk. Splitting it turned out to be three piles, not two: what's true of sipario, what's true of my talk, and what's true of *any* talk — templates compile, every class a template writes has a rule and every rule a class, every figure the deck names is on disk, every step has a script.

That third pile ships as `talkChecks()`, so a talk runs those rules over its own folder rather than keeping a copy of them. A copy in two repos is the first of them going stale, which is most of what this extraction existed to prevent.

Running them over the shipped example for the first time found a real bug. The example's own stylesheet had never carried the rules for fourteen classes its own templates write. The folder a stranger copies to start had been wrong the whole time, and only became visible once the rules stopped being one talk's private business.

## It ships no design

There are no themes, and there is no default look. A folder with no `templates/` is refused by name rather than quietly handed somebody else's design, because what a slide looks like is the talk's decision and guessing on its behalf is the engine making it. Two starters come in the box — `minimal`, and `stylish`, the same deck set like the bill outside an Italian theatre — and both carry their look entirely in `deck.css` and `templates/`.

Here's the same slide in each of them, a diff with the change coloured:

<img src="/assets/images/blog/2026-09-04-sipario-a-deck-engine-taken-out-of-a-talk/minimal-3.8.png"
  width="49%"
  alt="The diff slide in the minimal starter: a heading in Raleway over a teal rule, the diff in a dark block with the removed line in red and the added line in green, and the minimap and compass in the corners.">
<img src="/assets/images/blog/2026-09-04-sipario-a-deck-engine-taken-out-of-a-talk/stylish-3.8.png"
  width="49%"
  alt="The same slide in the stylish starter: the heading centred in Newsreader over a swelled red rule, the diff on a paper slip with the removed line struck through and the added line in red, and an italic caption under it.">

Same `deck.md`, same engine, and nothing in common between the two but the format.

The other thing it refuses is guessing about your slides. Ids and numbers are declared and then verified, never derived: a number that disagrees with where the slide actually sits stops the render and names both. So does a standfirst on a template that never prints one, or a step restating a line already on the stage. A slide that renders plausibly and wrongly is worse in a room than one that never rendered at all.

## In the room

0.2.0 added the three things I wanted once the engine was out and I could work on it on its own.

The slides move. Press the right arrow and the slide you're on goes out to the left, whole, as the next one comes in from the right. Left is the reverse, down and up do the same vertically inside a movement, and Home and End jump back or forward. A step inside a build moves nothing, because the point of a build is that nothing shifts while you add to it. Reduced motion turns all of it off.

That's less decorative than it sounds. Left and right are movements and up and down are slides within one, so the direction the stage moves is the room being told which of those just happened, without me saying it out loud.

There's a minimap by the top right corner of the slide now: a column per movement, a cell per slide, a build's cell marked, and the one you're on lit in your accent colour. It's on by default and M hides it. On by default because a key you have to know about is a feature nobody uses.

And the window around the slide is yours to set. Four tokens: `--surround` for the window itself, `--surround-ink` and `--surround-accent` for the compass and the minimap when they sit on it, and `--slide-edge` for a hairline the slide wears. They all default to what they were, so a talk that sets none of them looks exactly as it did. The minimal starter sets all four: its surround is the slide's own paper a shade sunk, so a slide moving across it crosses nothing darker than that tint, and the slide draws its edge in the theme's hairline to keep its shape.

Once the slides move, the surround stops being leftover letterboxing and becomes something a slide crosses, which makes it design, which makes it your talk's rather than mine.

## Try it

```sh
mkdir my-talk && cd my-talk
npm init -y
npm install sipario
npx sipario new         # a talk in ./talk, named from the folder
npx sipario serve       # http://localhost:9999
```

Node 22 or later, and nothing else — the server is Node's own `http`. No build step, so there's no artefact to go stale. No PDF export, no plugin API.

This is 0.2.0, so it's still early. MIT, at [github.com/pardel/sipario](https://github.com/pardel/sipario).

*Updated 2026-09-05 for 0.2.0: the moving slides, the minimap, and the tokens for the surround.*

*sipario is Italian for the theatre curtain, the one that rises to open a performance.*
