Michael profile

Live-tunable slides

The Why-We-Win slide is the riskiest slide in a pitch. It's a competitive comparison. The investor either nods because the matrix matches their mental model, or pushes back because it doesn't, and then the rest of the meeting is you defending the matrix.

The fix isn't to pick the perfect matrix in advance. It's to tune the matrix in front of the investor.

The pattern

A slide ships with two components:

import { WhyWeWinSlide, WhyWeWinControls } from "./why-we-win-simple-slide"
 
export const WhyWeWin: SlideDefinition = {
  id: "why-we-win",
  title: "Why we win",
  component: WhyWeWinSlide,
  controls: WhyWeWinControls,
  context: { ... },
}

The viewer renders component as the slide. If controls is defined, and you're in development, it renders that next to the slide in a yellow LOCAL panel — visible to the presenter, hidden in production builds, stripped out of the PDF export.

The slide has its own micro-IDE. Same screen. Same React tree. Different region of the viewport.

What the controls actually do

In the Why-We-Win matrix, the controls expose:

Live, in the slide. The investor says "I don't think Slack belongs in that quadrant." You drag the dot. They say "what about price vs depth?" You change the axes. They say "show me as cards." You toggle the layout. The slide reshapes.

This is impossible in PowerPoint. It's impossible in Tome. It's trivial in React.

Sharing state between slide and controls

Both components need to read and write the same state. Prop drilling doesn't work — the controls aren't a child of the slide. Context would, but it's overkill for one slide. The cleanest pattern is useSyncExternalStore against a module-level store:

// store.ts
let state = { layout: "matrix", x: "price", y: "depth", scores: { ... } }
const listeners = new Set<() => void>()
 
export const getState = () => state
export const setState = (patch) => {
  state = { ...state, ...patch }
  listeners.forEach(l => l())
}
export const subscribe = (l) => {
  listeners.add(l)
  return () => listeners.delete(l)
}
 
// hook.ts
export const useDeckState = () =>
  useSyncExternalStore(subscribe, getState, getState)

Both WhyWeWinSlide and WhyWeWinControls call useDeckState(). They re-render when the store changes. No provider, no context, no prop chain. The store lives at module scope, which is fine because the slide is a singleton in any given render.

Why this is better than prepping multiple decks

The alternative is "make six decks, one per likely audience." This is what people do today. It's a lot of work, and you usually pick the wrong one — the meeting goes somewhere you didn't predict.

Live-tunable slides are the opposite move. One deck. Tune at runtime. The slide that bends to the conversation is more powerful than the slide that anticipated it.

What disappears in export

The yellow LOCAL panel is wrapped in an isLocalEnv() check. In production it doesn't render. In the PDF export it doesn't render. The exported deck looks exactly like the live deck minus the controls. The investor leaves the meeting with a clean PDF; in the meeting itself, you had the IDE.

{controls && isLocalEnv() ? (
  <div className="absolute top-4 right-4 border-2 border-yellow-500 ...">
    <ControlsComponent />
  </div>
) : null}

One conditional. Four lines of CSS. That's the entire production-vs-local switch.

What else this unlocks

Anything that's interactive becomes a candidate. The constraint is no longer "can a slide do this?" The constraint is "should it?"

When not to use it

Most slides shouldn't have controls. Title slide doesn't need them. Team slide doesn't need them. The control slot is for slides where a parameter is genuinely uncertain and tuning it adds value.

A heuristic: if the slide is making a quantitative claim and you've ever wanted to "show the same chart with a different assumption," that slide is a candidate.

What it costs

A second component. A small store. A few minutes of plumbing. The first one feels overkill. The second one takes ten minutes and makes the slide three times better. By the third, it's just how you build interactive slides.

nopoint ships the yellow-LOCAL-panel pattern, the isLocalEnv() helper, and a useSyncExternalStore template. You bring the slide.