MDX: prose slides
Write Markdown; element tags (h1, p, ul, code, …) map to themed components automatically. Best for text-heavy slides. Fenced code is syntax-highlighted at build time.
MDX: prose slides
Write Markdown; element tags (h1, p, ul, code, …) map to themed components automatically. Best for text-heavy slides. Fenced code is syntax-highlighted at build time.
TSX: interactive slides
A default-exported React component. Best for anything animated, data-driven, or that places a <Plugin>. Full access to Motion, the engine, and your own components.
Both compile to the same thing; pick per slide and mix them in one deck.
Every slide is authored on a logical 1280×720 canvas (STAGE_W × STAGE_H). The engine’s ScaledStage scales that canvas to fit any screen, centered and letterboxed, so a slide looks identical on a laptop, a projector, and a phone (just at a different scale). You never write responsive breakpoints for slide content. You design once at 1280×720.
SlideFrame gives each slide a brand background, an animated atmosphere, and a padded content area. Break out with absolute positioning for full-bleed visuals.
In MDX you write plain Markdown and the look comes from the brand. The mapping (in @liebstoeckel/components):
| Markdown | Renders as |
|---|---|
# H1 | display serif, text-text |
## H2 | text-primary heading |
| paragraph | font-body, text-muted |
| list item | accent bullet (▹) |
`inline code` | a themed pill |
```ts block | Shiki-highlighted, brand-bound colors |
Magic Move re-renders content, which would reset a stateful element (an <iframe>, a <video>, a running widget). For those, the engine provides a persistent layer: render the element once at the deck root and project it into slides via a <Slot>. It keeps its internal state while traveling between slides.
The two are complements: Magic morphs different stateless elements into one another (declared inline per slide); <Slot> carries one stateful element across slides (defined once). Reach for <Slot> when state must survive, Magic for a stateless flourish.
<Present slides={[...]} persistent={[{ id: "live", render: () => <LiveIframe /> }]}/>import { Slot } from "@liebstoeckel/engine";
## The clock keeps running →
<Slot id="live" className="mt-10 h-56 w-full rounded-2xl border border-surface" />Move the <Slot id="live"> to a different position on the next slide and the live element animates to its new home without reloading.
The element is positioned onto the slot on the current slide, and how it gets there depends on where you came from:
<Slot> with the same id. The element springs from the old position/size to the new one, state intact. This is the headline trick: put the slot in two consecutive slides at different spots to make a live widget glide across.It’s positioned in the deck’s logical canvas space, so it lines up with the slot at any stage scale (fullscreen, windowed, or mobile). Reloading is never triggered by any of these: the element is rendered once and never unmounts.
How one slide gives way to the next is configurable. Set a deck-wide default on Present, and override any individual slide with an export const transition:
<Present slides={[...]} transition="slide" /> {/* deck default; omit for "fade" */}export const transition = "zoom"; // this slide onlyBuilt-in presets: fade (default), blur, slide (directional push, mirrors on back-nav), zoom, none. Need something bespoke? Pass a custom spec instead of a name:
export const transition = { variants: { enter: { opacity: 0, y: 40 }, center: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -40 } }, transition: { duration: 0.5 },};Transitions respect prefers-reduced-motion (they collapse to a tiny fade), and don’t affect persistent-layer elements: those travel above the swap untouched.
On mobile (touch / coarse pointer) transitions are off by default. The stage is heavily down-scaled there, so cuts feel snappier and avoid jank. Opt back in with the mobileTransitions escape hatch:
<Present slides={[...]} transition="slide" mobileTransitions />Code-first presentations your agent can author. One file out, no server.
Comments