|
| 1 | +--- |
| 2 | +name: deckx |
| 3 | +description: Create a deck with deckx. Use when the user mentions "deckx", "deck" or "slides", asks to build a slide deck from MDX, asks to convert a brand palette into a deck stylesheet, or asks how to convert a deckx HTML deck into a PDF. Covers project layout, deckx.toml config, deck.mdx authoring, custom React components, the styles.css token contract, and the Chrome headless PDF command. |
| 4 | +--- |
| 5 | + |
| 6 | +# deckx |
| 7 | + |
| 8 | +`deckx` builds a single, self-contained HTML slide deck from one MDX file plus a CSS theme and an optional folder of React components. The HTML is print-ready and converts to PDF via Chrome headless. |
| 9 | + |
| 10 | +## Installation |
| 11 | + |
| 12 | +```bash |
| 13 | +bun init -y # if you don't already have a package.json initialized |
| 14 | +bun add @samuelcolvin/deckx |
| 15 | +``` |
| 16 | + |
| 17 | +The npm package is `@samuelcolvin/deckx`; the installed CLI binary is `deckx`. `npm i` / `pnpm add` work the same way. Inside `deck.mdx` you always import from `"deckx"` (a Vite alias, not the npm name). |
| 18 | + |
| 19 | +## Project layout |
| 20 | + |
| 21 | +``` |
| 22 | +my-deck/ |
| 23 | +├── deckx.toml # config: title, theme, tabs, footer, paths (all optional) |
| 24 | +├── deck.mdx # the slides |
| 25 | +├── styles.css # CSS variable overrides (optional) |
| 26 | +└── components/ # optional custom React/TSX components |
| 27 | + └── Hello.tsx |
| 28 | +``` |
| 29 | + |
| 30 | +```bash |
| 31 | +bunx deckx dev # dev server with HMR on http://localhost:5173/ |
| 32 | +bunx deckx html # build to ./dist/index.html |
| 33 | +bunx deckx pdf # build HTML, then ./dist/deck.pdf via Chrome headless |
| 34 | +``` |
| 35 | + |
| 36 | +`html` and `pdf` accept an optional output-path positional - e.g. `bunx deckx pdf my-deck.pdf` or `bunx deckx html out/slides.html`. Use `--dir <dir>` to point at a build directory other than the current one. To convert an existing HTML file to PDF without rebuilding, use `bunx deckx html-to-pdf <input.html> <output.pdf>`. |
| 37 | + |
| 38 | +## `deckx.toml` |
| 39 | + |
| 40 | +All fields are optional - a deck with only `deck.mdx` works. |
| 41 | + |
| 42 | +```toml |
| 43 | +title = "My Deck - April 2026" # browser tab title |
| 44 | + |
| 45 | +# light | dark | markdown-light | markdown-dark (default: light) |
| 46 | +# "markdown-*" variants render source-style decorations on top: |
| 47 | +# heading "#"/"##" prefixes, "**" strong markers, traffic-light dots, |
| 48 | +# mono slide counter, diamond bullets. |
| 49 | +theme = "light" |
| 50 | + |
| 51 | +# Small footer rendered bottom-right of every slide. |
| 52 | +footer = "Confidential - do not share" |
| 53 | + |
| 54 | +# Path to a favicon for the browser tab. .svg / .png / .ico / .jpg. |
| 55 | +# Inlined as a data URI so the deck stays self-contained. |
| 56 | +favicon = "assets/favicon.svg" |
| 57 | + |
| 58 | +# Path overrides (defaults shown). |
| 59 | +mdx = "deck.mdx" |
| 60 | +styles = "styles.css" |
| 61 | +components = "components" |
| 62 | + |
| 63 | +# Optional tab nav. When present, <Slide tab="..."> highlights the matching tab. |
| 64 | +tabs = [ |
| 65 | + { id = "intro", label = "Intro" }, |
| 66 | + { id = "details", label = "Details" }, |
| 67 | +] |
| 68 | +``` |
| 69 | + |
| 70 | +If `tabs` is omitted, the `tab` prop on `<Slide>` is ignored and slides render with a plain title topbar. |
| 71 | + |
| 72 | +## `deck.mdx` |
| 73 | + |
| 74 | +Standard MDX. Import `Slide` from `"deckx"`, plus any custom components from `./components/`. |
| 75 | + |
| 76 | +```mdx |
| 77 | +import { Slide } from "deckx"; |
| 78 | +import Hello from "./components/Hello.tsx"; |
| 79 | + |
| 80 | +<Slide layout="title" title="Investor Deck / April 2026"> |
| 81 | + |
| 82 | +### Section Label |
| 83 | + |
| 84 | +# My Deck |
| 85 | + |
| 86 | +## A subtitle |
| 87 | + |
| 88 | +</Slide> |
| 89 | + |
| 90 | +<Slide tab="intro"> |
| 91 | + |
| 92 | +# Hello world |
| 93 | + |
| 94 | +- Bullet one |
| 95 | +- Bullet two |
| 96 | + |
| 97 | +<Hello name="friend" /> |
| 98 | + |
| 99 | +</Slide> |
| 100 | + |
| 101 | +<Slide layout="statement"> |
| 102 | + |
| 103 | +# One big idea. |
| 104 | + |
| 105 | +</Slide> |
| 106 | +``` |
| 107 | + |
| 108 | +**Do NOT wrap slides in `<div className="deck">`** - deckx adds it for you. |
| 109 | + |
| 110 | +### `<Slide>` props |
| 111 | + |
| 112 | +All props are optional. A bare `<Slide>` renders a regular content slide using the deck theme. |
| 113 | + |
| 114 | +#### `layout` - structural layout (default `'content'`) |
| 115 | + |
| 116 | +Picks how the slide arranges its body. Adds a `.<value>-slide` class to the `.slide` element so you can target each variant from `styles.css`. |
| 117 | + |
| 118 | +- `'content'` (default) - regular slide. Headings, paragraphs, bullets, code, and tables flow top-down inside `.slide-body`. Use for the bulk of your deck. |
| 119 | +- `'title'` - cover / section slide. Bottom-aligns the hero; h1 is 4rem with tight letter-spacing; h2 renders in `--accent`. Pair with a leading `### Section Label` for a mono uppercase eyebrow. |
| 120 | +- `'statement'` - centered one-liner. The body is centered both vertically and horizontally; h1 is 3.4rem; paragraphs cap at 80% width. Use for transitions between sections or "one bold idea" beats. |
| 121 | + |
| 122 | +#### `theme` - color variant (defaults to the deck theme) |
| 123 | + |
| 124 | +Per-slide palette override. |
| 125 | + |
| 126 | +- Omit (or pass `'dark'`) - the slide inherits the deck-level `theme` from `deckx.toml`. |
| 127 | +- `'light'` - forces a single slide onto the light palette (`--bg-light`, `--color-text-light`, `--color-heading-light`) regardless of the deck theme. Useful when one slide needs to break out - e.g. a screenshot of a light-themed UI on an otherwise dark deck. Adds `.light-slide` to the slide. |
| 128 | + |
| 129 | +There is no inverse override: on a light deck, `theme="dark"` has no effect. If you need a single dark slide on a light deck, target it from CSS with a custom `id` or class. |
| 130 | + |
| 131 | +#### Other props |
| 132 | + |
| 133 | +- `tab` - string matching an `id` from the `tabs` array in `deckx.toml`. Replaces the plain topbar title with the tab bar, with this slide's tab highlighted. Clicking any tab in any slide jumps to the first slide whose `tab` matches. If `tabs` is not configured, the prop is silently ignored. |
| 134 | +- `title` - plain text rendered in the topbar when `tab` is not set. Also drives `document.title`, so the browser tab updates as the active slide changes. Ignored when `tab` is set. |
| 135 | +- `space` - `'tight'` reduces bullet/paragraph spacing (use when a slide is close to overflowing); `'wide'` increases padding and line-height (use for slides with very little text where you want generous breathing room). |
| 136 | +- `fontSize` - `'large'` bumps body text from 1.15rem to 1.35rem, and h1/h2 proportionally. Useful for slides that need to read from the back of a room. |
| 137 | +- `id` - sets the HTML `id` on the underlying `<section>`. Useful for targeting one slide from `styles.css` (`.slide#hero { ... }`) without adding a custom class. |
| 138 | + |
| 139 | +### MDX gotchas |
| 140 | + |
| 141 | +- Use `-` (hyphen-minus), never `—` (em dash). |
| 142 | +- Keep blank lines around block elements inside a slide, but **not** between the last block and the closing `</Slide>` - MDX misparses a trailing blank. |
| 143 | +- `### Section Label` at the top of a slide renders as a diamond + uppercase mono label. |
| 144 | +- Slides must fit **279.4mm × 157.2mm** (16:9). If overflowing, try `space="tight"` first, then drop content. |
| 145 | + |
| 146 | +### Images |
| 147 | + |
| 148 | +Place images in your project (e.g. `assets/`) and import as ES modules: |
| 149 | + |
| 150 | +```mdx |
| 151 | +import logo from "./assets/logo.png"; |
| 152 | +<img src={logo} alt="Logo" /> |
| 153 | +``` |
| 154 | + |
| 155 | +Vite inlines them into the final HTML. The markdown `` syntax does **not** get inlined - always use `<img src={imported} />`. |
| 156 | + |
| 157 | +### Code blocks |
| 158 | + |
| 159 | +Fenced code blocks are syntax-highlighted at build time by [Shiki](https://shiki.style). Tag the fence with a language so tokens get coloured: |
| 160 | + |
| 161 | +````mdx |
| 162 | +```ts |
| 163 | +export function greet(name: string): string { |
| 164 | + return `hello, ${name}`; |
| 165 | +} |
| 166 | +``` |
| 167 | +```` |
| 168 | + |
| 169 | +All Shiki bundled languages work without any per-deck configuration. Highlighting happens during the build, so the output HTML carries only inline-styled `<span>`s - no grammar files, no runtime highlighter. |
| 170 | + |
| 171 | +Both a light and a dark theme are emitted into every code block. deckx switches between them automatically based on the slide's effective theme: dark deck themes (`dark`, `markdown-dark`) use the dark code theme, and `<Slide theme="light">` always shows the light code theme even inside a dark deck. Pick the two themes in `deckx.toml`: |
| 172 | + |
| 173 | +```toml |
| 174 | +code_light_theme = "github-light" # default |
| 175 | +code_dark_theme = "github-dark" # default |
| 176 | +``` |
| 177 | + |
| 178 | +Browse the available themes at <https://shiki.style/themes>. |
| 179 | + |
| 180 | +## Custom components |
| 181 | + |
| 182 | +Any `.tsx` file in `components/` (or wherever `deckx.toml` `components` points) can be imported into `deck.mdx` with a relative path: |
| 183 | + |
| 184 | +```tsx |
| 185 | +// components/Hello.tsx |
| 186 | +export default function Hello({ name }: { name: string }) { |
| 187 | + return <p>Hello, {name}!</p>; |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +```mdx |
| 192 | +import Hello from "./components/Hello.tsx"; |
| 193 | +<Hello name="world" /> |
| 194 | +``` |
| 195 | + |
| 196 | +React 19 is available. Components see the same CSS variables your `styles.css` defines, so to stay on-brand, read from variables (`color: var(--accent)`) rather than hard-coding colors. |
| 197 | + |
| 198 | +## Authoring `styles.css` |
| 199 | + |
| 200 | +The base stylesheet handles all layout, typography, slide dimensions, the topbar, transitions, and the PDF `@page` setup. `styles.css` only needs to override CSS variables on `:root` to set brand tokens. |
| 201 | + |
| 202 | +### Variable contract |
| 203 | + |
| 204 | +Backgrounds: |
| 205 | + |
| 206 | +- `--bg-deck` (default `#0d0d0d`) - background outside the slide, presenter mode only. |
| 207 | +- `--bg-slide` (default `#1a1a1a`) - default slide background. |
| 208 | +- `--bg-light` (default `#ffffff`) - slide bg for `light` / `markdown-light` decks and `<Slide theme="light">`. |
| 209 | +- `--surface` (default `#2a2a2a`) - inline code background, table headers. |
| 210 | + |
| 211 | +Text: |
| 212 | + |
| 213 | +- `--color-text` (default white @ 85%) - body text on dark slides. |
| 214 | +- `--color-heading` (default `#ffffff`) - h1, h2, h4, strong on dark slides. |
| 215 | +- `--color-muted` (default `#8f888e`) - heading prefixes, slide counter, subdued UI. |
| 216 | +- `--color-text-light` (default `#2a2230`) - body text on light slides. |
| 217 | +- `--color-heading-light` (default `#1a1018`) - headings on light slides. |
| 218 | + |
| 219 | +Accents: |
| 220 | + |
| 221 | +- `--accent` (default `#4a9eff`) - primary accent: bullets, h3, links, blockquote bar. |
| 222 | +- `--accent-secondary` (default `#ff6b6b`) - em, link hover. |
| 223 | +- `--accent-tertiary` (default `#b388ff`) - hr gradient stop. |
| 224 | +- `--accent-aqua` (default `#4ad7c5`) - inline code text, active tab, topbar tabs. |
| 225 | + |
| 226 | +Fonts: |
| 227 | + |
| 228 | +- `--font-body` (default system sans stack) - body and headings, unless `--font-heading` overrides. |
| 229 | +- `--font-heading` (default inherits body) - headings. |
| 230 | +- `--font-mono` (default system mono) - inline code, code blocks, tabs, counter, h3. |
| 231 | +- `--font-terminal` (default inherits body) - body inside `.slide-body`. |
| 232 | + |
| 233 | +### CSS class hooks |
| 234 | + |
| 235 | +For finer control beyond the variable contract, target these classes from `styles.css`. Most decks won't need them - prefer overriding variables first. |
| 236 | + |
| 237 | +Slide structure: |
| 238 | + |
| 239 | +- `.deck-presenter` - outermost wrapper. Owns the viewport background and the fit-to-window scaling transform. |
| 240 | +- `.deck` - inner slide stream (direct child of `.deck-presenter`). |
| 241 | +- `.slide` - a single slide (`<section>`). Sized 16:9, holds topbar + content + optional footer. |
| 242 | +- `.slide-topbar` - 48px window-chrome bar at the top of every slide. |
| 243 | +- `.slide-content` - padded body wrapper below the topbar (this is what `--slide-padding` applies to). |
| 244 | +- `.slide-body` - inner MDX content container, descendant of `.slide-content`. |
| 245 | +- `.slide-footer` - bottom-right footer text, rendered when `footer` is set in `deckx.toml`. |
| 246 | + |
| 247 | +Slide modifiers (added to `.slide` based on props): |
| 248 | + |
| 249 | +- `.title-slide` - `layout="title"`, bottom-aligned hero. |
| 250 | +- `.statement-slide` - `layout="statement"`, centered hero. |
| 251 | +- `.light-slide` - `theme="light"`, forces the light palette. |
| 252 | +- `.space-tight` / `.space-wide` - vertical spacing density. |
| 253 | +- `.font-large` - bumps body text size. |
| 254 | + |
| 255 | +Topbar - left (traffic lights + title/tabs): |
| 256 | + |
| 257 | +- `.topbar-dots` - the traffic-light anchor (clicking jumps to slide 1). Only visible under `markdown-*` themes. |
| 258 | +- `.topbar-dot` plus `.topbar-dot--red` / `.topbar-dot--yellow` / `.topbar-dot--green` - individual dots. |
| 259 | +- `.topbar-title` - plain title text, shown when `<Slide title="...">` is set without a `tab`. |
| 260 | + |
| 261 | +Topbar - tabs (rendered when `<Slide tab="...">` is set and `tabs` are configured in `deckx.toml`): |
| 262 | + |
| 263 | +- `.topbar-tabs` - the tab bar container. |
| 264 | +- `.topbar-tab-group` - per-tab wrapper containing the link plus its leading separator. |
| 265 | +- `.topbar-tab-sep` - the `→` glyph between tabs. |
| 266 | +- `.topbar-tab` - the tab link. |
| 267 | +- `.topbar-tab--active` - applied to the currently-selected tab. |
| 268 | + |
| 269 | +Topbar - right (prev/next + counter): |
| 270 | + |
| 271 | +- `.topbar-nav` - container holding the prev/next buttons and slide counter. |
| 272 | +- `.topbar-nav-btn` plus `.topbar-nav-prev` / `.topbar-nav-next` - the nav buttons (auto-hidden in print). |
| 273 | +- `.topbar-nav-counter` - the `01/12` slide counter. |
| 274 | + |
| 275 | +Deck-level theme classes (applied to both `<html>` and `.deck-presenter` based on `theme` in `deckx.toml`): |
| 276 | + |
| 277 | +- `.theme-light` / `.theme-dark` / `.theme-markdown-light` / `.theme-markdown-dark` |
| 278 | + |
| 279 | +MDX content inside `.slide-body` renders as plain HTML (`h1`-`h4`, `p`, `ul`, `ol`, `pre`, `code`, `table`, `blockquote`, `a`, `img`, `hr`) - target those tags directly with `.slide <tag>` selectors rather than expecting deckx to add wrapper classes. |
| 280 | + |
| 281 | +### Mapping a brand palette |
| 282 | + |
| 283 | +1. Pick the **most distinctive** brand color, assign to `--accent`. Pick a warm counterpoint as `--accent-secondary`. |
| 284 | +2. Pick a slightly off-white for `--color-heading` (pure white reads sterile under projector light). |
| 285 | +3. Pick a tinted dark for `--bg-slide` (pure black is harsh). |
| 286 | +4. For light slides, pick a tinted light bg (cream, eggshell, lavender - not pure white) plus a near-black text color → `--bg-light` / `--color-heading-light` / `--color-text-light`. |
| 287 | +5. For custom fonts, self-host woff2 files in `assets/` and declare them with `@font-face` in `styles.css`, then point `--font-body` / `--font-mono` at the family. Do **not** use `@import url(...)` from Google Fonts - CSS spec requires @import to come before all other statements, which Vite's CSS bundling routinely violates when concatenating the base stylesheet with yours. `@font-face` can appear anywhere. Use [Google Webfonts Helper](https://gwfh.mranftl.com/fonts) to download woff2 files; Vite inlines them into the deck, keeping it self-contained for offline / PDF export. |
| 288 | + |
| 289 | +```css |
| 290 | +@font-face { |
| 291 | + font-family: 'YourFont'; |
| 292 | + src: url('./assets/YourFont-Regular.woff2') format('woff2'); |
| 293 | + font-weight: 400; |
| 294 | + font-display: swap; |
| 295 | +} |
| 296 | + |
| 297 | +:root { |
| 298 | + --bg-slide: #...; /* tinted dark */ |
| 299 | + --bg-light: #...; /* tinted light */ |
| 300 | + --surface: #...; |
| 301 | + |
| 302 | + --color-heading: #...; /* off-white */ |
| 303 | + --color-text: rgba(...); |
| 304 | + --color-heading-light: #...; |
| 305 | + --color-text-light: #...; |
| 306 | + |
| 307 | + --accent: #...; /* signature brand color */ |
| 308 | + --accent-secondary: #...; /* warm counterpoint */ |
| 309 | + |
| 310 | + --font-body: 'YourFont', system-ui, sans-serif; |
| 311 | + --font-mono: 'YourFontMono', ui-monospace, monospace; |
| 312 | +} |
| 313 | +``` |
| 314 | + |
| 315 | +## Building & PDF |
| 316 | + |
| 317 | +`bunx deckx pdf` is the easy path: it builds the HTML, prints the exact Chrome command it's about to run, then runs it. Output lands at `./dist/deck.pdf`. |
| 318 | + |
| 319 | +If Chrome / Chromium can't be found, copy the printed command and run it yourself with the right binary path. On Linux deckx auto-detects `google-chrome`, `google-chrome-stable`, `chromium`, or `chromium-browser`. |
| 320 | + |
| 321 | +Paper size in the printed command matches the slide dimensions (11in × 6.1875in = 16:9). If you override `--slide-width` / `--slide-height` in `styles.css`, edit the `--paper-*` flags to match before running. |
| 322 | + |
| 323 | +To spot-check the PDF (requires `pdftoppm` from poppler): |
| 324 | + |
| 325 | +```bash |
| 326 | +mkdir -p ./tmp && pdftoppm -r 100 ./dist/deck.pdf ./tmp/page -png |
| 327 | +``` |
| 328 | + |
| 329 | +One PNG per slide lands in `./tmp/`, gitignore that path. |
0 commit comments