# Troubleshooting

> Symptoms, what causes them, and the fix — including the failures that produce no error

Entries are grouped by what you **observe**, not by what is wrong, because you
arrive here with a symptom rather than a diagnosis. The headings are the literal
strings you would search for.

Before reading further: `npx @getnarro/cli check deck.md` diagnoses most of this
in about a second and names the value that would have worked. Roughly half the
entries below exist because the failure happens *without* an error, which is
exactly when a checker is worth more than a message.

## Failures that produce no error at all

These are the expensive ones. Nothing throws, the build succeeds, and the deck
is wrong on screen.

### A directive shows up as literal text on the slide

**Symptom:** the audience sees `<!-- .element: class="fragment" -->` or
`::: note` rendered as body copy, instead of the effect it was supposed to have.

**Cause:** that is Marp, reveal.js, or Slidev syntax. Narro's dialect is
different, and markdown it does not recognise is just text — there is nothing
for it to reject. This is overwhelmingly the most common broken deck when the
markdown was written by an AI, because those three tools dominate its training
data.

**Fix:** the Narro equivalents are `{.class}` after an element, `<!-- notes: … -->`
for speaker notes, and `{.step}` on a list item for a fragment. Full dialect:
[Markdown mode](/docs/markdown-mode). If a model wrote the deck, paste the
dialect contract from [Rules for AI agents](/docs/agent-rules) into your
project's `AGENTS.md` — otherwise it will regenerate the same syntax next time.

**`narro check` catches this**, including when the syntax is Narro's own and the
element simply did not form. To print the markers on purpose, put them in a code
fence or `` `inline code` ``, or escape them — `\*\*not bold\*\*`.

### Everything after `::something::` vanishes

**Symptom:** half a slide is missing. No error, no warning, no blank space
where it should have been.

**Cause:** the slot name does not exist on that layout, so nothing renders it.
`two-column` has one named slot and it is called `right`; `::sidebar::` and
`::second::` are inventions.

**Fix:** run `check`, which reports this one precisely:

```
error  deck.md:10 (slide 2)
  Layout "two-column" has no slot "sidebar", so everything after `::sidebar::`
  would not render. Slots for this layout: right.
```

Slot names describe the **position**, not the content. See the layout table in
[Markdown mode](/docs/markdown-mode).

### The deck renders with default styling and ignores the theme

**Symptom:** a `theme:` is set in the deck frontmatter, and the deck looks like
it has no theme.

**Cause:** the theme name does not resolve. Theme ids are kebab-case and
qualified — `corporate-dark`, not `corporate`; `tech-dark`, not `tech`.

**Fix:** `check` catches this as an error and lists every installed theme.
Valid ids are
<!-- generated:themes-inline -->`default`, `corporate-dark`, `minimalist-light`, `creative-gradient`, `ocean-blue`, `forest-green`, `sunset-warm`, `midnight-purple`, `tech-dark`, `startup-bold`, `academic-classic`<!-- /generated:themes-inline -->.

### A frontmatter key does nothing

**Symptom:** you set `aspect-ratio: "16:9"` (or `color-scheme`, or
`font-preset`) and the deck ignores it.

**Cause:** frontmatter keys are camelCase. An unrecognised key is not an error —
a theme or a custom layout is allowed to read its own keys — so it is kept and
ignored.

**Fix:** `check` reports it as a warning with the correction:

```
warning  deck.md
  Deck frontmatter key "aspect-ratio" is not one the dialect defines, so nothing
  reads it unless your theme or layouts do. Did you mean "aspectRatio"?
```

Deck keys are
<!-- generated:deck-frontmatter -->`title`, `author`, `date`, `theme`, `template`, `aspectRatio`, `transition`, `class`, `keyboard`, `mouse`, `touch`, `routing`, `favicon`, `maxDuration`<!-- /generated:deck-frontmatter -->;
slide keys are
<!-- generated:slide-frontmatter -->`layout`, `class`, `id`, `transition`, `background`, `notes`<!-- /generated:slide-frontmatter -->.

### A list renders with plain bullets when you asked for checkmarks

**Symptom:** `<List variant="bullet">` or `variant="number"` produces the
default marker.

**Cause:** neither value exists. `List`'s `variant` names the CSS marker, not
the semantic role:
<!-- generated:type:List.variant -->"disc" | "decimal" | "check" | "arrow" | "none"<!-- /generated:type:List.variant -->.

**Fix:** `variant="disc"` for bullets, `variant="decimal"` for numbers,
`variant="check"` for checkmarks. The [component
reference](/docs/components) is generated from the types, so it is the
authority on every prop.

### You wanted a horizontal rule and got a new slide

**Symptom:** a `---` written as a divider inside a slide's body starts a new
slide instead of drawing a line.

**Cause:** `---` on its own line *is* the slide separator. There is no way for
the splitter to tell a thematic break from a slide break, so the slide break
wins.

**Fix:** use `***` or `___`, which markdown renders as the same horizontal rule
and the splitter ignores. A `---` inside a fenced code block — backticks or
tildes — is safe and does not split.

## Errors, with their fixes

### `Could not parse expression with acorn`

**Symptom:** a deck fails on a line containing `{`.

**Cause:** a slide is MDX, so `{` opens a JavaScript expression unless the block
is Narro's attribute syntax — `{.class}`, `{#id}`, `{key=value}`. A brace around
anything else, `{like this}`, is read as code.

The message names the offending character when it can:
*"Narro's attribute syntax does not accept `X`."* Today the only character that
cannot appear in a class is a literal `{` or `}`; arbitrary values
(`text-[10px]`), opacity modifiers (`bg-white/5`), fractions, variants
(`hover:`, `md:`), half-steps (`p-1.5`) and `!` overrides are all accepted. If
you are on an older version, those all failed here — upgrade rather than
rewriting the deck.

**Fix:** write `\{` for a literal brace, or put the text in `` `inline code` ``.
If the block really is a class list containing a brace, move that one class into
`deck.css`.

### `… is not attribute syntax, so this container has no classes at all`

**Symptom:** `check` or `build` fails on a `:::` line, or on a fenced code
block's info string.

**Cause:** a `{ … }` in either position has no second reading — unlike a brace in
prose, which MDX can compile as an expression — so a block that is not attribute
syntax there means the whole class list would be discarded. It used to be
discarded silently: the deck built, `check --strict` said `✓ looks good`, and the
box reached the projector unstyled.

**Fix:** make it attribute syntax (`.class`, `#id`, `key=value`), or remove it. A
fence's info string is `` ```bash {.rounded-xl} `` — language first, block last.

### `… written here but on no element in the compiled slide`

**Symptom:** `check` names a class you wrote and says nothing renders it.

**Cause:** the block parsed, the class is legal, and the element it attached to
does not exist in the output — most often because the block sits after something
that is not an element (an MDX comment, a slot marker) and had nothing to attach
to.

**Fix:** run `narro check deck.md --render` and look at the element you meant. An
attribute block styles the element it sits against: a list item styles the
`<li>`, a fence styles the `<pre>`, a block on its own line styles the block
above it, and one written tight against a word styles the word. Move the block
next to the element you meant.

### `N slides do not fit the canvas`

**Symptom:** `check --fit` reports overflow and names an element.

**Cause:** the slide's content is taller or wider than the canvas (1920×1080 by
default), so the audience sees it clipped. Nothing else in the toolchain can see
this — a build is silent about layout.

**Fix:** reduce a font size, a padding, or the amount on the slide; or split the
slide. The named element is the last one sticking out, which is usually the one
to shrink.

### `--fit needs a browser and none is installed`

**Symptom:** `check --fit` measures nothing.

**Cause:** `--fit` renders the deck for real, so it needs Chromium. It reports
this rather than passing quietly, and under `--strict` it fails.

**Fix:** `npx playwright install chromium`, or set
`NARRO_CHROMIUM=/path/to/chromium` if your image already has one.

### `Expected a closing tag for <br>`

**Symptom:** a deck that used to build now fails on a line of raw HTML.

**Cause:** raw HTML in a slide is JSX. Earlier versions compiled `.md` decks as
plain CommonMark, which silently *deleted* the tag rather than complaining — so
a deck could carry HTML that never rendered and never said so.

**Fix:** close every tag (`<br />`, `<img … />`) and camelCase every attribute
(`className`, not `class`). If the HTML was never rendering anyway, deleting it
changes nothing on screen.

### `Unknown layout "x". Available: …`

**Symptom:** `check` or `build` exits non-zero with this message.

**Cause:** the `layout:` value is not a built-in and there is no matching file
in a `layouts/` directory beside the deck.

**Fix:** use one of
<!-- generated:layouts-inline -->`default`, `cover`, `section`, `quote`, `end`, `two-column`, `three-column`, `image-right`, `image-full`<!-- /generated:layouts-inline -->,
or create `layouts/<name>.tsx` next to the deck — the error names that path for
you. `check` also suggests the closest existing name, so a typo is a one-line
fix.

Note that `layout` means two different things and they are separate closed
sets. In slide frontmatter it is the layout name above. On the React
`SlideContent` component it is content alignment:
<!-- generated:type:SlideContent.layout -->"default" | "centered" | "top" | "bottom"<!-- /generated:type:SlideContent.layout -->.

### `has no exported member 'Heading'` / `does not provide an export named 'Heading'`

**Symptom:** TypeScript reports `Module '"@getnarro/core"' has no exported
member 'Heading'` (TS2305), or the browser reports `The requested module
'@getnarro/core' does not provide an export named 'Heading'`. The package
itself resolves fine; only the named import fails.

**Cause:** `@getnarro/core` is the presentation runtime plus the motion and
canvas primitives —
<!-- generated:components:core -->`AnimatePresence`, `Canvas`, `CanvasElement`, `CanvasShape`, `ErrorBoundary`, `ErrorOverlay`, `MarqueeRow`, `MasonryBackground`, `Motion`, `MotionContainer`, `MotionList`, `MotionNumber`, `MotionPresence`, `MotionSpotlight`, `MotionStep`, `MotionSteps`, `MotionText`, `MotionTransform`, `NebulaBackground`, `Notes`, `OverviewGrid`, `Presentation`, `PresenterNotes`, `Slide`, `SlideContent`, `TransformSlide`, `TrustedByMarquee`<!-- /generated:components:core -->.
`Heading` is not among them. The content components —
<!-- generated:component-count:shared-ui -->53<!-- /generated:component-count:shared-ui -->
of them, including `Heading`, `Text`, `List`, `Code`, and `Image` — are in
`@getnarro/shared-ui`.

**Fix:** import content components from `@getnarro/shared-ui`. The split is
deliberate: `shared-ui` has no runtime dependency on `core`, so a deck can use
the components without the presentation runtime. Which package exports what:
<https://getnarro.com/components.json>, or one component at a time at
<https://getnarro.com/llms/Heading.md>.

### `npx narro …` installs something that is not Narro

**Symptom:** `npx narro build deck.md` downloads a package and then fails in a
way that mentions no Narro command you recognise.

**Cause:** the npm package named `narro` is **unrelated to this project**.
Narro's packages are all published under the `@getnarro/` scope, and the
`narro` binary comes from `@getnarro/cli`.

**Fix:** `npx @getnarro/cli <command>`. Inside a scaffolded project the local
binary is already on the path, so plain `narro build deck.md` works there;
outside one, always name the scoped package.

**Anti-fix:** installing `narro` globally so the bare command resolves. It is
someone else's package, and it will not become Narro.

### The deck builds locally and fails in CI or on a fresh clone

**Symptom:** works on your machine, fails after `npm install` somewhere else.

**Cause:** almost always a dependency that resolves through your lockfile but
is not declared, or a file that is not in the package's `files` list.

**Fix:** reproduce it the way a user hits it — install into a directory outside
your project and build there. Narro's own repository runs exactly this as a
separate CI job (`pnpm test:templates`) because the workspace build stayed
green while every published template failed to build, for months.

## Still stuck

`check` reports every problem it finds in one run, so fix them all before
re-running rather than one at a time. If the deck passes `check` and is still
wrong, the problem is in a place `check` cannot see — a custom layout
component, a theme's own CSS, or an MDX component — and `build` with the
browser console open is the next step.

Something that should have been caught and was not is worth
[reporting](https://github.com/getnarro/narro/issues): a recurring
troubleshooting entry is a checker feature that has not been written yet.