π§βLimitations
What can't Narro do, and what do I do instead?
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.
There is no diagram primitive
Diagram draws ASCII art. FlowDiagram and ProcessFlowChart draw linear step
chains. Nothing draws a nested tree, a mapping between two structures, or boxes
joined by labelled connectors β the shapes a technical deck asks for most.
This is the largest thing Narro does not have. It is not a small gap and this page will not pretend otherwise.
Instead: build it out of containers and Tailwind. Every author who has hit
this reinvented the same thing, so here it is once, ready to paste. Nested :::
containers give the boxes; twelve lines of deck.css give the connectors.
/* deck.css β a row of boxes joined by arrows. */
.rs-flow {
display: flex;
align-items: stretch;
gap: 3rem;
}
.rs-flow > .rs-container {
position: relative;
display: flex;
align-items: center;
}
/* The line, and then the head, both centred on the gap before each box after
the first. `top: 50%` is load-bearing: without it an absolutely positioned
pseudo-element falls to its static position, and the arrowhead lands under
the line instead of on it. */
.rs-flow > .rs-container + .rs-container::before {
content: "";
position: absolute;
top: 50%;
right: 100%;
width: 3rem;
height: 2px;
transform: translateY(-50%);
background: currentColor;
opacity: 0.35;
}
.rs-flow > .rs-container + .rs-container::after {
content: "";
position: absolute;
top: 50%;
right: calc(100% - 1px);
transform: translateY(-50%);
border: 7px solid transparent;
border-left-color: currentColor;
opacity: 0.35;
}
:::{.rs-flow}
:::{.rounded-xl .border .border-white/20 .p-6}
**Deck**
:::
:::{.rounded-xl .border .border-white/20 .p-6}
**Compile**
:::
:::{.rounded-xl .border .border-white/20 .p-6}
**Slides**
:::
:::
For a tree rather than a row, nest the containers and swap flex for
flex-col, using ml-8 border-l on the children to draw the trunk. For anything
where the connectors cross β a mapping between two columns, a cascade that skips
a level β the honest answer is an SVG: draw it in whatever you draw diagrams in,
and use {.w-full}. Nothing in Narro will do it for you.
Chart is deliberately small
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.
Instead: for anything analytical, render it with a real charting library and
put the result in a slide. Chart is for the one number a slide is about.
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. Without a browser it reports that
nothing was measured and, under --strict, fails; it never reports a pass it did
not verify.
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.
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.
Code has no syntax highlighting
Code renders a styled block with the language recorded as a class and no
tokenising. For a deck where four slides carry code, that is a visible ceiling β
the clean example and the ugly one look equally grey.
Instead: highlight it yourself and drop the result in β Shikiβs
codeToHtml at build time, or any highlighter that keys off the
language-* class the fence already carries.
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.