npm i @getnarro/core

Ship the story,
not the slides.

Narro is a React library for narrative, built for you and your coding agent. A deck is source: your agent writes it, narro check tells it what is wrong, and the same file renders as an animated presentation, a static deck, a PDF, or a video.

Build your first deck
npm create narro@latest my-story

Working through an agent? Point it at getnarro.com/llms.txt — the dialect, every valid name, and the command that checks its work.

Live — nothing here is a screenshot

A text file, becoming four different things.

Left: what you or your agent types. Right: what it is, the moment it is typed. One source, rendered as a deck, a chart that queries, a film.

4 files · 4 renderers · 0 exports by hand

Markdown → deck01 / 04
waiting for the file…narro check --fit ✓ names valid, every slide fitsnarro dev story.tsx ✓ stage runningpnpm test ✓ 4 tests, the cohort is livenarro-video check . ✓ 1:00, measuredwaiting for the file…
Q3 review01layout: cover

We shipped the wrong thing.

Six weeks in, the roadmap was fiction. Here is what it cost us.

6 weeksSpent
2 rewritesShipped
1 lessonKept
01 / 21
<Motion effect="slideUp" />

Thenwewatchedasinglesession.

One recording did what four dashboards could not.

Tension
Reveal
Pause
<Retention live />warehouse · 2026-Q1

Retention, live from the warehouse.

100%
92%
81%
74%
70%
68%
67%
66%
66%
W1W4W8queried at render, not pasted in
VIDEO EXPORT · 1920×10800:41 / 1:00

So we rebuilt around the story.

So we rebuilt around it.

01 / 04
One source →narro devAnimated decknarro buildStatic decknarro dev deck.mdMarkdown → deck@getnarro/coreFull React decknarro-video renderVideonarro export --pdfPDF

AI-first

Written for the thing writing your deck.

A deck is source, so an agent can write one — and then, unlike a slide editor, actually check its work. Narro publishes the documentation, the closed sets of valid names and the component types as machine-readable files, and ships the loop that turns a wrong deck into an error message. The four runs below are how Narro’s own pitch was written; every file they name is in the repository.

You

Read https://getnarro.com/llms.txt. Write our pitch as one deck.md — a table for the competitor slide, a diagram fence for the agent loop — then run npx @getnarro/cli check deck.md --fit until it passes.

markdown mode · one file, nothing to scaffold

deck.mdsource →
---
title: Narro
template: ./narro.template.ts
layout: title
---

# A deck is text.

So it diffs. It reviews. It regenerates.
$ npx @getnarro/cli check deck.md --fit

deck.md looks good · 21 slides fit the canvas

You

Now pull every color and size out of deck.md into a template beside it. Name the layouts after what they do, and put a footer and a slide number on every slide except the title.

a deck template · the slide master, as data

narro.template.tssource →
export default {
  tokens: { colors: { signal: "#ff5c38" } },
  master: {
    footer: { text: "{title} · {date}" },
    slideNumber: { format: "{n} / {total}", from: 2 },
  },
  layouts: { chapter: { base: "section" } },
} satisfies DeckTemplate;
$ npx @getnarro/cli check deck.md

template: narro-pitch · 12 layout ids the deck may name

You

Rebuild it in React, and make the closed-set slide real: import builtinLayoutNames from @getnarro/markdown and check whatever I type against it, live, with the near-miss suggestion.

React mode · for the one slide a picture cannot make

src/slides/ClosedSetSlide.tsxsource →
import { builtinLayoutNames } from "@getnarro/markdown/layouts";

const nearest = [...builtinLayoutNames]
  .sort((a, b) => distance(typed, a) - distance(typed, b))[0];

return `✗ "${typed}" is not a layout. Did you mean "${nearest}"?`;
$ pnpm --filter @getnarro/pitch-deck test

4 tests passed · the nine names come from the import, not the slide

You

Take the six strongest beats and make a sixty-second video out of them. Narration only — do not type a single duration, and give me 16:9 and 9:16 from one layout.

@getnarro/video · every duration measured, none typed

script.tssource →
export const script = defineScript({
  voice: "af_heart",
  cues: [
    { id: "hook", text: "A deck is text.", scene: "title" },
    { id: "climb", text: "You climb it without rewriting the deck.",
      scene: "ladder", hold: 12 },
  ],
});
$ npx narro-video check .

no overflow in landscape or vertical · runtime 1:00, measured

The whole setup
Read https://getnarro.com/llms.txt, then write the Q3 review to deck.md and run npx @getnarro/cli check deck.md --fit until it passes.

No plugin, no configuration. The docs are tested this way too: an eval hands a model llms.txt and nothing else, then puts the deck it writes through narro check and narro build.

Rules to paste into your AGENTS.md →
/llms.txt
The index an assistant fetches first. Every page is markdown at its own URL with .md appended, and /llms-full.txt is all of them in one request.
/catalog.json
Every layout, theme, color scheme, template, frontmatter key and CLI command that exists. A model choosing from it cannot invent a name that reads plausible and renders wrong.
/components.json
Every component’s props, read out of the TypeScript types by the compiler rather than transcribed. The frontmatter has JSON Schema, at /schema.
/cmdspec.json
The CLI in cmdspec: every command and option with its type and default, what each command changes, what it prints and what its exit status means — the parts --help leaves out. narro --cmdspec prints the same document.
narro check --fit
The loop an agent can run itself: names validated without a build, and --fit measures whether the content actually fits the canvas.
docspack ask "…"
Install the docs as a docspack and the agent asks instead of reading: at most three chunks and 3,000 tokens, answered from a local index with no network call.
@getnarro/mcp-server
Twenty-four MCP tools for an assistant that has no shell — scaffold a deck, rewrite one slide, apply a theme, build, export, and eleven more that drive the whole video loop.

The deck itself

Not a screenshot. The deck.

This is Narro’s own pitch, running here rather than pictured here — the same build narro build deck.md produces, embedded unmodified. Twenty-one slides, written as one markdown file with a slide master beside it: every color, rail, footer and slide number below comes from that one template, and not one of them is in the deck.

Click the deck, then ← →. 21 slides, one markdown file.

deck.md
The deck. Names its layouts by id and carries no color or size of its own — which is why it survives a rebrand.
narro.template.ts
The slide master, as data: tokens, the chrome every slide carries, and the twelve layout ids the deck may name.
pitch/STORY.md
The argument the deck is one rendering of. No number reaches a slide that is not in its claims table.

One source, six destinations

Write it once. Send it anywhere it needs to land.

narro dev

Animated deck

Timed reveals, transitions and stage direction. Runs in the browser at 60fps.

narro build

Static deck

Same story, built to files you can host anywhere or hand over.

narro dev deck.md

Markdown → deck

Point it at a markdown deck. Headings become slides, frontmatter becomes props.

@getnarro/core

Full React deck

Slides are components. Hooks, state, effects, your own charts and canvases.

narro-video render

Video

MP4, timed by the narration rather than by a timeline. Edit a sentence, and the cut moves with it.

narro export --pdf

PDF

One page per slide, every fragment revealed. The version that gets forwarded.

Video

Edit the sentence. The video re-times itself.

A composition is React and its narration is a list of sentences. Each one is synthesized and measured, so a scene is as long as what is said in it — and there is no timeline to maintain. Change a word and the cut moves, the captions re-page and the .srt regenerates. Nothing is re-cut, because nothing was cut by hand.

0:00 / 1:00
1:00 — 1920×1080 · six beats · thirteen narrated cues, every one measured and captionedRead the script it was cut from →
script.ts
export const script = defineScript({ voice: "af_heart", cues: [ { id: "hook", scene: "title", text: "A deck is just text." }, { id: "claim", scene: "title", text: "So is a video." }, ], });

The durations live nowhere in this file, and nowhere in any other. They are what the audio measured.

How the video pipeline works →
narro-video render

Only what changed

A render is split into scene-aligned pieces, each keyed on its own source, narration, brand and settings. Rewrite one sentence and one scene re-renders.

narro-video review

The director’s pass

Frozen holds, dead air, pace, caption contrast, the band a platform draws its own chrome over. Every finding names the sentence at fault and the file that decides it.

@getnarro/video/tts

Any voice, per sentence

Kokoro runs offline and needs no key. Bring ElevenLabs, OpenAI or a local model instead, clone your founder’s voice once, or give a cue a second speaker.

narro-video record

Footage that re-records itself

A screen recording as a JSON spec, captured on virtual time so two takes match frame for frame. --check re-records and tells you the product moved.

narro-video stale

Claims that still hold

Every sentence can cite its source. The render ships the claims manifest, and this says which cited page has changed since — so the library cannot quietly go out of date.

narro-video brief --locale

Every locale, no re-cut

The German runs thirty percent longer and nothing is re-cut: each scene grows to fit its own narration, and the captions and the .srt follow.

Recording

A demo of your product that is not a video.

narro record captures what a page is made of — the DOM as it stood, every change that followed, and the bytes of everything it drew with — and replays it as real DOM. Sharp at any projector size, kilobytes rather than megabytes, and embeddable in a slide, a site, or one file you email.

narro record https://getnarro.com/docs --embed ./public/replay
0:00 / 0:13
13 seconds of this site’s documentation, 90 KB of DOM. Select the text in it — the replay holds while you read, and runs on when you let go.How recording works →
--html demo.html

One file you can email

The player, the recording and every font and image it draws with, in a single HTML file that opens from a USB stick. No server, no network, nothing to embed.

--embed ./public

Or three files a site caches

The player is the same bytes for every demo, so it loads once. A page then pays for the event stream — tens of kilobytes for a flow.

replay fence

A fence in a markdown deck

src: /demo.narrocast and it plays on the slide, at any projector size, with a still for the PDF export that cannot run JavaScript.

--cursor trail

The pointer, and its path

A replay with no cursor shows the result of a click and never the click. Recorded flows draw the pointer, and optionally the line it took.

Terminal

A terminal demo, in a form that can go stale out loud.

A session is a file: the commands, the pauses, the moments worth naming — and, once you have run it, the bytes they actually printed. Because it is text, it diffs, it re-themes, it renders at any size, and CI can tell you the demo no longer matches the tool. Write it, or perform it once with tape and get the file for free. Rendering never runs anything; only record does, and it asks first.

$ narro new demo.md
✓ Created demo.md

  Start editing:  npx @getnarro/cli dev demo.md
  Check it:       npx @getnarro/cli check demo.md

$ narro check demo.md

  narro 0.8.2 · dialect @getnarro/markdown 0.9.2 · core 0.6.2
✓ demo.md looks good
$
0:00 / 0:03
3.2s of this checkout’s own CLI, 84×13, shipped as 1.6 KB of playbook — not the 24 KB GIF or the 14 KB SVG of the same session — and every character in the frame is text you can select.Read the session it plays →
demo.tape
Type "npm create narro@latest my-deck" Enter Wait /Scaffolded/ Mark ready

VHS syntax, so a demo you already wrote imports unchanged — plus the four directives it has no equivalent for: a mark, a mask, a cue and an expectation.

Or skip writing it: narro-terminal tape demo.tape opens a shell, and when you exit it has written this from what you typed — and the playbook and capture from what the commands printed.

How terminal playbooks work →
narro-terminal record --check

The demo that fails when it lies

Re-runs the commands and diffs what they printed against the capture beside them. A GIF regenerates happily after the tool stopped saying what the demo says; this exits 1 and names the command.

narro-terminal tape

Perform it once, get all three

Use a real shell and exit. What you typed becomes the tape; what the commands printed lands in the playbook and the capture. Typos are folded away, long pauses capped, and nothing runs twice.

narro-terminal render

A GIF at the size you asked for

Differenced frames, a palette counted rather than guessed, per-frame delays, and a --max-mb ladder that retries until it fits. No ffmpeg.

narro-terminal render --svg

Vector, and no browser

Rows are interned and referenced, so a text session is usually smaller as vector than as a GIF. Nothing is rasterized, so it renders where Chrome cannot be installed.

<narro-terminal>

Embed the session, not a video of it

One script tag, 14.7 KB gzipped, no framework. The page ships the playbook and plays it — seekable, selectable, and re-themable from the same file.

term / tape fences

A terminal on a slide

Paste a transcript, or write a tape. Resolved at build time to positioned HTML, so it works in a deck, a PDF and a PPTX with nothing to load — and it never runs anything.

narro-terminal import

Bring what you already have

asciicast v2 and v3, VHS .tape files and ttyrec, read by what is in the file rather than its name. Export writes asciicast back out.

Zooming canvas

A camera that moves, without making anyone queasy.

Put a diagram on a plane and fly a camera between named framings, so the deck moves the way the idea does. The reason nobody does this well is not the idea — it is the interpolation. Tween the center and the zoom separately, as every tool in this field does, and a flight speeds up by an order of magnitude on the way in while the numbers insist it is slowing down. That mismatch is what the audience feels.

Independent tweensWhat every other zooming presentation ships
IngestPOST /eventsQueueat-least-onceStoragecolumnar, 90d

3.54×variation in perceived speed across the flight

van Wijk & NuijWhat an atlas flies
IngestPOST /eventsQueueat-least-onceStoragecolumnar, 90d

1.00×variation in perceived speed across the flight

one path, two interpolations

Same two waypoints, same duration, same frame. The only difference is the path between them.

deck.md
```atlas scene: /diagram.svg --- - id: whole at: 960, 540, 1920 - id: storage at: 1250, 545, 340 hold: 2 ```

A waypoint frames a rectangle in world units, never a zoom level — so the same path frames the same content in a 16:9 deck, a vertical video and a phone. Leave the duration out and the camera works out how long the move wants to take from how far the picture actually travels.

Zooming canvas →

It refuses paths nobody could watch

A zoom jump past 8×, a duration nowhere near what the move needs, a framing that cannot be rendered, a selector that matches nothing. narro check reports all of it before anyone sees the deck.

Sharp at rest, cheap in flight

One composited transform, whatever the scene holds. will-change is switched on for the move and off on arrival, so the frames anyone actually reads are re-rastered and sharp.

Reduced motion cuts, it does not hide

A viewer who asks for less motion gets a cut between waypoints. Every framing stays reachable; only the swoop goes.

The same path in a video

The camera is a pure function of the frame, so a rendered frame is identical alone, out of order, or on another machine. No requestAnimationFrame anywhere in the render.

The primitives

A deck has slides. A story has a shape.

Narro ships the beats a story is made of, so pacing is something you write down instead of something you hope for on the day.

  1. 01

    Tension

    A slide that holds keeps the room on the problem.

  2. 02

    Reveal

    <MotionStep> lands the answer on your cue.

  3. 03

    Pause

    Silence you can schedule, in milliseconds.

  4. 04

    Surprise

    Break your own grid on purpose, once.

  5. 05

    Resolution

    The ask, and where it leaves them.

Examples

Decks you can open, fork, present.

Browse all examples →

Docs

Quickstart

01 — install
npm i @getnarro/core @getnarro/cli

Adds @getnarro/core and the CLI. React 18+ and Node 20+.

02 — write a story
import { Motion, Notes, Presentation, Slide }
  from '@getnarro/core';
import { Heading, Text } from '@getnarro/shared-ui';

export default function Story() {
  return (
    <Presentation aspectRatio="16:9">
      <Slide id="intro">
        <Heading>We shipped the wrong thing.</Heading>
        <Notes>Slow down here.</Notes>
      </Slide>
      <Slide id="fix">
        <Motion effect="slide-up" delay={0.8}>
          <Text>Here is what we learned.</Text>
        </Motion>
      </Slide>
    </Presentation>
  );
}
03 — present it
narro dev story.tsx

Opens the stage with hot reload, speaker notes and a presenter clock.

04 — ship it
narro build
narro export --pdf
narro export --to pptx

Same source, every artifact. Nothing to re-lay-out by hand.

Docs

API reference

Full reference →

<Presentation>

The root of a story. Owns navigation, routing and the viewport scaling that keeps a deck legible from a laptop to a projector.

aspectRatio
"16:9" | "4:3" | "1:1"
Stage shape. Defaults to 16:9; slides letterbox rather than reflow.
theme
PresentationTheme
Fonts, palette and slide defaults for the whole deck.
autoPlay
boolean | number | number[]
Advance unattended. A number sets the dwell in ms, an array sets it per slide.
onSlideChange
(index: number) => void
Fires on every transition. Useful for analytics or captions.
maxDuration
number
Talk length in minutes, counted down in the presenter panel.
routing
boolean
Reflect the current slide in the URL so any frame is deep-linkable. On by default.

<Slide>

One frame of the story. Any React tree inside, including your own components.

id
string
Stable anchor. Deep-linkable, and the key used in exports and comments.
layout
LayoutType
Built-in composition — title, split, quote and the rest — or bring your own.
background
string
Any CSS color or image value, applied to the slide root.
transition
"none" | "fade" | "slide" | "zoom"
How this slide arrives. Overrides the deck default.
subSlides
SubSlide[]
Steps within one frame, for content that builds rather than cuts.

Beats

Timing components. They read the slide’s activation state, so a beat behaves the same on stage, in a static export and under a video render.

<Motion>
effect, delay, duration
Animate children in when the slide becomes active.
<MotionSteps>
totalSteps, loop
A sequence advanced by your keypress, not by a timer.
<MotionStep>
step: number
Shows once the sequence reaches its step. The deliberate reveal.
<MotionText>
MotionTextProps
Per-word or per-character entrance for a line that needs to land.
<Notes>
children
Speaker notes. Presenter view only, stripped from every export.

Hooks

Read and drive the story from inside your own components.

useNavigation()
NavigationState & Actions
Current slide plus next, previous, goToSlideById and fullscreen.
useSlideIndex()
number
The active slide index, for progress bars and chapter marks.
useSlideActive()
boolean
True while this slide is on stage. Gate your own animations on it.
useFragmentStep()
number
The current step inside a slide, for custom step-aware components.
useSlideSync()
SlideSyncControls
Drive a second screen, or follow one, over a sync channel.

CLI

One binary for the whole loop: author, check, present, export.

narro new
Scaffold a starter markdown deck.
narro dev <entry>
Stage with hot reload and presenter view.
narro check --fit
Validate a deck, and measure whether content actually fits.
narro build
Build for production. Pass a .md file for markdown mode.
narro export --pdf
PDF, one page per slide, every fragment revealed.
narro export --to pptx
PowerPoint, for the room that needs a .pptx.
narro bundle
One self-contained HTML file, nothing to serve.

Markdown

Front matter maps onto deck and slide props, so a markdown deck can grow into a React one without a rewrite.

title
string
Deck title. Window title and export metadata.
theme
string
A marketplace theme name, or a path to a local one.
template
string
The slide master this deck inherits from.
id
string
Per-slide. The same stable anchor <Slide id> takes.
layout
string
Per-slide. Resolved against your dir, then the theme, then built-ins.
notes
string
Per-slide speaker notes, without the HTML comment.
---
title: Q4 roadmap
theme: default
aspectRatio: "16:9"
---

# We shipped the wrong thing. {.text-7xl}

- Six weeks
- Two rewrites
- One good lesson

<!-- notes: Lead with the cost, then the lesson. -->

Your next deck is already a repo.