Skip to content

The authoring model

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):

MarkdownRenders as
# H1display serif, text-text
## H2text-primary heading
paragraphfont-body, text-muted
list itemaccent bullet ()
`inline code`a themed pill
```ts blockShiki-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.

main.tsx
<Present
slides={[...]}
persistent={[{ id: "live", render: () => <LiveIframe /> }]}
/>
a slide
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:

  • Travel: both the slide you left and the one you entered have a <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.
  • Appear: you arrive from a slide that has no matching slot (or it’s the first time it shows). The element snaps into the slot and only fades in. It won’t fly in from wherever it last was.
  • Leave: the next slide has no matching slot. The element fades out in step with the slide transition (it doesn’t linger after).

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:

main.tsx
<Present slides={[...]} transition="slide" /> {/* deck default; omit for "fade" */}
a slide
export const transition = "zoom"; // this slide only

Built-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:

main.tsx
<Present slides={[...]} transition="slide" mobileTransitions />

Comments

liebstoeckel

Code-first presentations your agent can author. One file out, no server.

© 2026 Leon Kaucher