# Limitations

> What Narro cannot do, and what to reach for 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](/docs/markdown-mode.md#which-tailwind-classes-it-accepts),
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.

```css
/* 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;
}
```

```markdown
:::{.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 `![](./diagram.svg){.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](/docs/markdown-mode.md#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:

```bash
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`](#check---fit-needs-a-browser) 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.