πŸš§β€‚Limitations

What can't Narro do, and what do I do instead?

11 min readView as markdown

Every entry here is something an author found by hitting it while writing a deck. Knowing the shape of the box up front is worth more than discovering an edge at the wrong moment, so this page is deliberately blunt.

If something is missing here and surprised you, that is a bug in this page.

Two restrictions that used to live here are gone, in case you have read an older copy: the attribute syntax now accepts every Tailwind class, including arbitrary values, opacity modifiers and variants; and Card, Badge and Callout exist, so a bordered panel or a pill is no longer hand-rolled.

Diagrams draw a graph, and only a graph

A ```diagram fence draws boxes joined by arrows β€” chains, fan-in, fan-out, trees and two-column mappings, all of which are the same layered graph to it. See Markdown mode for the syntax.

What it will not do, and what to reach for instead:

Not drawnInstead
A cycle β€” A -> B -> AReported as an error rather than drawn wrong. Break the loop, or use an image
Manual positioningThe layout is derived from the edges; there is no way to place a box by hand
Edges routed around obstacles, or crossing deliberatelyAn SVG you drew elsewhere: ![](./diagram.svg){.w-full}
Anything that is not a node-and-edge graph β€” a timeline, a matrix, a mapNested ::: containers, or an image

It is sized by its content, in the same 1920Γ—1080 canvas units as the rest of the slide, so a diagram with many long labels can be wider than its slot, and a deep direction: down graph can be taller than the canvas. narro check <deck> --fit is what tells you; scale: 0.6 in the fence is what fixes it, and it shrinks the layout box along with the drawing. Shortening a label or switching direction still helps, but neither is required any more β€” and neither is a deck.css with zoom in it, which is what an author with no scale: had to reach for. Below about 0.4 the labels stop being readable, and the diagram wants fewer nodes instead.

For a shape the fence cannot express, containers still work and are still the answer β€” nested ::: blocks give you boxes, and a deck.css gives you whatever connectors you draw yourself.

Chart is deliberately small

There are two: a ```chart fence that draws a horizontal bar chart with no import (see Markdown mode), and the <Chart> component, which needs MDX or React. Everything below applies to both, and the fence is the smaller of the two: bars only, no legend, no title.

Bar, line, pie and donut, drawn with CSS and SVG. Bars can be horizontal or vertical, can print their values, and can carry a unit or a valueFormat; there is no axis, no gridline, no tooltip, no time series, and no stacking. A bar chart therefore has no scale reference β€” the bars are proportional to each other and to nothing else.

It also needs room. height covers the whole component β€” title and legend included β€” and below roughly 200px the bars stop carrying the comparison even though they still draw. Give a bar chart the height of a column, not the height of a caption.

One piece of scale you can pin. Bars are drawn against the largest value in their own chart, so two charts on two slides are quietly incomparable β€” a 60 and a 90 can be the same length on consecutive slides, and a reader comparing them gets a wrong answer with nothing to warn them. max sets the value a full-length bar represents, on the component and on the fence:

```chart
max: 100
unit: "%"
Before: 62
After: 91 *
```

That is not an axis and is not a step toward one.

This is the ceiling, on purpose. A slide that needs gridlines, a time series or a stacked series does not want a smaller chart library β€” it wants a real one. Render the figure with whatever your data already lives in, export it, and place it:

![Latency, p50 and p99, by week](./latency.svg){.w-full}

An SVG scales with the canvas, stays sharp on a projector, and costs the deck nothing at runtime. Chart is for the one number a slide is about; a ![]() is for everything else, and neither is a workaround for the other.

Icon is a small set of symbols, and has no logos

"star" | "arrow" | "check" | "cross" | "arrow-left" | "arrow-up" | "arrow-down" | "heart" | "warning" | "info" | "question" | "plus" | "minus" | "code" | "terminal" | "link" | "email" | "phone" | "location" | "calendar" | "clock" | "file" | "folder" | "search" | "settings" | "user" | "users", drawn as

Unicode glyphs. The names are a closed set and are typed, so an invented one is a compile error rather than a slide with the word rocket on it.

There is no brand mark and there will not be one. github used to be in the set, drawn as ⌘ β€” the Command key β€” which is a wrong answer rather than a rough one, and narro’s own component gallery showed it. An icon library that resolves names at runtime cannot be tree-shaken, so shipping one would put every glyph in every deck, on a runtime this page already apologises for two sections down.

Instead: for a logo, use the file.

![](./github-mark.svg){.h-12}

Literal colours do not follow the theme

theme: tech-dark restyles headings, prose, code and the built-in layouts. It does not restyle {.bg-cyan-400}, because that names a palette colour rather than a design token β€” so a deck that hand-rolls its diagrams, cards and charts with absolute Tailwind colours is effectively single-theme: change the theme and half the deck moves while the other half does not.

Instead: name the role, not the hue. Every theme’s colours are emitted as Tailwind theme tokens, so bg-accent, bg-background, bg-foreground, bg-muted, bg-primary, bg-secondary β€” and the matching text-*, border-* and ring-* β€” are ordinary utilities that move with theme:. They work from the attribute syntax, in single-file mode, with no companion file:

:::{.border-l-4 .border-accent .bg-foreground/5 .p-6}

Restyles itself with the deck.

:::

currentColor, bg-white/5, border-white/10 and the opacity-* scale also track the theme, and are the right answer for a tint rather than a role. Reach for a literal colour when the colour is the point, as in a chart series where red means β€œworse”, and expect to edit it if you re-theme.

See Theming for the full set, which is also in catalog.json under themeUtilities.

Markdown mode cannot loop

There is no iteration, no reuse, and no reference-and-repeat. Twelve cards is twelve containers: a twelve-item grid is around 120 lines of near-identical markdown, and a change to the shape is twelve edits. Nothing about it is blocked β€” it is just long, and it is the sharpest edge of the mode.

Instead: past about six repeated items, MDX and a .map() is 15 lines and one place to edit. One import in the deck preamble buys it; the deck stays a markdown file.

check --fit needs a browser

Every other check reads the compiled deck and needs nothing. --fit measures real layout, so it needs Chromium β€” npx playwright install chromium, or point NARRO_CHROMIUM at one your image already has. It also looks for a browser your machine already has when Playwright’s own resolution comes up empty, which is the common case under npx: the CLI gets its own copy of Playwright, and its bundled revision need not be the one your image installed. Without a browser it reports that nothing was measured and fails; it never reports a pass it did not verify. --fit-if-available downgrades that to a warning when you want the rest of check to run anyway.

There is no way to know a slide fits without rendering it, so there is no browser-free version of this check to build.

Markdown mode has no per-slide state

A slide cannot hold a value that changes as the audience watches β€” a selected swatch, a filtered table, a live counter. {.step} reveals content that was always there; it does not compute anything.

Instead: write a component and mount it. In a markdown deck, import it in the preamble; the component is ordinary React and can hold whatever state it likes.

Reveal order is per slide, not per deck

{.step} and <Fragment> reveal in source order within their slide, and every slide starts again from nothing. There is no deck-wide sequence, and no way to carry a revealed state across a slide boundary.

Two animation systems

CSS-keyframe components live in @getnarro/shared-ui (Fragment, Animation, AnimatedList, WordReveal); Framer Motion components live in @getnarro/core (Motion, MotionText, MotionNumber, MotionSteps). They overlap β€” CountingNumber and MotionNumber both count, TypewriterText and MotionText type="typewriter" both type β€” and their durations use different units: milliseconds in shared-ui, seconds in core.

Which to use: the Motion* components when you want physics, spring easing or layout animation; the shared-ui ones for a simple reveal. If you are choosing between two components with similar names, they do the same thing and either is fine.

Mind the unit when you move a number between them. <Fragment duration={300}> is 300ms; <Motion duration={300}> is five minutes. The overlap is a deferred decision and the unit mismatch is the sharp edge on it β€” check which package the component came from before copying a duration across.

Workflow cannot be stepped by the presenter. It is the only component that draws connectors between steps, and its animation prop is

`none`, `fade`, `progressive`, `flow` β€”

all of them on the deck’s clock, none on a keypress. So a slide that wants β€œreveal these three steps one at a time, with arrows between them” has to choose: Workflow for the connectors, or Fragment for presenter control. There is no combination that gives both.

No live data

A deck is a static build. Nothing polls, subscribes, or refetches on its own.

Instead: fetch in a component with useEffect like any React app, or bake the number in at build time β€” which is usually what a deck wants, since a presenter needs to know what the slide says before they say it.

Export needs a browser

narro export --pdf and --png drive Playwright. In CI that means installing it:

npm install -D playwright && npx playwright install chromium

Without a browser: narro check deck.md --render prints what each slide renders β€” elements, classes and text β€” which covers β€œdid that class land where I meant” without a screenshot. It cannot tell you whether the slide fits; only --fit can, and that needs the browser too.

The React Code component is not highlighted

Markdown fences are. A ```ts fence is tokenised by Shiki when the deck compiles, so the keyword, the string and the number are different colours on the slide. It costs the browser nothing: the highlighting happens at build time and what ships is coloured markup, not a highlighter. A fence with no language, or one naming a language Shiki has no grammar for, renders as plain text exactly as before.

The React Code component is a different situation. It takes its code as children at runtime, and a React deck has no narro compile step to tokenise them in β€” highlighting it would mean shipping a highlighter and its grammars to the browser, which for a library that already apologises for a 114 KB floor is a worse trade than grey code.

Instead, in React mode: run a highlighter yourself and drop the result in. Shiki’s codeToHtml at build time produces markup that keys off the same language-* class. Or write the sample as a markdown fence in a markdown deck, where it is done for you.

The runtime brings Framer Motion

@getnarro/core depends on framer-motion, and it ships whether or not a deck uses a Motion* component. Measured: a three-slide markdown deck with no components at all is 114 KB gzipped, and adding a <Motion> to it costs 1 KB β€” which is the proof that the library was already there. A ten-slide deck using a few shared-ui components lands around 130 KB. Almost none of that is your content, and there is no lighter entry point.

If bundle size is the point of your deck: narro bundle produces one self-contained HTML file, narro export --png produces images, and neither carries a runtime at all.

The npm name narro is someone else’s

npx narro downloads an unrelated package. The CLI is @getnarro/cli, and every Narro package is under the @getnarro/ scope. There is no fix for this beyond using the scoped name.

IntroductionWhat is Narro, and should I be writing markdown or React?
InstallationWhat do I install, and what does the project look like afterwards?
First DeckWhat does a working React deck look like, end to end?
Markdown ModeHow do I split a slide, style one word, or reveal a line β€” without leaving the markdown file?
Writing AI PromptsWhat do I tell an AI so the deck it writes actually builds?
Rules for AI AgentsWhat do I paste into my repo so an agent stops writing decks that build wrong?
React APIWhich component or hook do I import, and what does it take?
CLIWhich command do I run, and what are its flags?
Markdown APIHow do I read, edit, or validate a deck file from my own code instead of by hand?
Component ReferenceWhat props does this component take, and which package do I import it from?
Verifying a deckHow do I know my deck is actually correct?
AnimationHow do I reveal a list one line at a time, or move between slides with something other than a cut?
NavigationHow does the audience move through the deck, and how do I present it?
Canvas & PositioningHow do I put something at an exact position instead of in the flow?
Images & MediaHow do I use an image as a background, tint it, or embed a video?
ArchitectureWhich package owns what, and why is the seam where it is?
Transform ModeHow do I zoom and pan across one big canvas instead of cutting between slides?
Import & ExportHow do I get this deck out as PPTX, PDF, or one file I can email?
AI IntegrationHow do I wire an AI assistant up to Narro so it can write and build decks?
ThemingHow do I change the colours, fonts, and overall look of a deck?
Deck TemplatesHow do I define one house style with named layouts my slides can reference, like a PowerPoint master?
TroubleshootingSomething is wrong with my deck. What is it, and how do I fix it?
LimitationsWhat can't Narro do, and what do I do instead?