---
title: Troubleshooting
description: The errors a deck actually prints, what each one means, and what to change. Search this page for the message you got.
sidebar:
  icon: life-buoy
---

Two commands answer most of it before you read further.

```bash
deckard validate   # the slides changed
deckard doctor     # a deck that used to work stopped
```

`validate` loads the deck and checks its slides and its theme. `doctor` checks the app around it: Node, whether `@thebuilder/deckard-core` resolves, whether the stylesheet is imported, whether the four route files still re-export their adapter, and whether `deck/deck.ts` loads. Both exit 1 on a problem, and `doctor` keeps going so you see all of them at once.

## A slide does not appear

**Symptom.** You added `deck/slides/roadmap.slide.tsx` and the deck still has the old slide count. No error.

**Cause.** Either the file is not in the glob, or the dev server is serving stale modules.

Discovery matches one pattern:

```tsx
import.meta.glob("./slides/**/*.slide.tsx", { eager: true })
```

A file is picked up only if it is under `deck/slides/` and its name ends in `.slide.tsx`. `roadmap.tsx` is not, and neither is a file next to `slides.tsx` instead of inside `slides/`. Nothing warns about a file that misses.

**Fix.** Check the name and the directory, then restart `next dev`. Adding or deleting a matched file while the dev server runs leaves the page routes serving the modules they had at boot, so a correctly named file can still be missing until you restart.

`deckard validate` prints the count it actually resolved:

```text
deck   6 slides: 4 inline, 2 discovered
```

If the discovered number is not what you expect, it is the glob. If it is right and the browser disagrees, it is the dev server.

## Discovered slide "…" has no default export

**Symptom.**

```text
Discovered slide "slides/roadmap.slide.tsx" has no default export. A slide module exports its component as the default, plus optional meta and notes.
```

**Cause.** The module exports the component by name.

**Fix.** Export it as the default. `meta` and `notes` stay named:

```tsx deck/slides/roadmap.slide.tsx
export const meta: SlideMeta = { slug: "roadmap", title: "Roadmap" }
export const notes = "Skip the middle row if time is short."

export default function RoadmapSlide() {
  return <OpenContentSlide title="Roadmap">{/* … */}</OpenContentSlide>
}
```

## Discovered slide "…" is an async module

**Symptom.**

```text
Discovered slide "slides/api.slide.tsx" is an async module, so the glob handed back a promise instead of its exports. Import it in the deck array with slideFromModule, or drop the dependency that uses top-level await.
```

**Cause.** The module, or something it imports, uses top-level await or WebAssembly. The eager glob then returns a promise instead of the exports.

`CodeBlock` is the usual one. Shiki loads a WebAssembly regex engine, which is why it ships from `@thebuilder/deckard-core/code-block` and not from the components barrel. A discovered slide that imports it becomes async.

**Fix.** Take that slide out of discovery and wire it into the array by hand:

```tsx deck/slides.tsx
import * as apiSlide from "@/deck/slides/api.slide"
import { slideFromModule } from "@thebuilder/deckard-core/slide-from-module"

export const slides: SlideDefinition[] = [
  ...discovered,
  slideFromModule(apiSlide, "deck/slides/api.slide.tsx"),
]
```

The file keeps its name and its place, and stops going through the glob.

## Duplicate or invalid slug

All four fail the deck at load, so they surface from `deckard validate`, from `doctor`, and from the build.

**Two slides on one slug.**

```text
Slides 4 and 5 both use the slug "keyboard". Slide ids must be unique.
```

Rename one. The numbers are 1-based positions in the deck.

**A numeric slug.**

```text
Slide 3 has the numeric slug "2024". A numeric slug adds nothing over /slides/3 and breaks when slides move, so use letters or drop the slug.
```

**Characters that are not safe in a path.**

```text
Slide 3 has the slug "Q4 Results", which is not safe in a URL path. Use lowercase letters, digits, and hyphens.
```

`q4-results` works.

**An empty slug.**

```text
Slide 3 has an empty slug. Give it a value or drop the slug so the slide is served at /slides/3.
```

## A slide overflows the canvas

**Symptom, in the browser.** An amber ring around the canvas under `next dev`, and a console line:

```text
Slide content runs 794px below the canvas and is clipped.
```

**Symptom, in CI.**

```text
/slides/roadmap  slide content runs 794px below the canvas
1 of 6 slides overflow the canvas and are clipped. Trim the content, or wrap the part that has to scroll in SlideScrollArea.
```

**Cause.** The canvas is a fixed box and it clips silently. The three measured regions are the slide frame, the header, and the footer, so `the header` or `the footer` in that line means the chrome grew, not the body.

**Fix.** Cut a sentence. That is the right answer most of the time and the slide is usually better for it. When the content genuinely has to be there, wrap the part that can scroll:

```tsx
<SlideScrollArea label="Full configuration" maxHeight={360}>
  <ConfigTable />
</SlideScrollArea>
```

A stepped slide is measured fully revealed, because hidden steps keep their space in the layout. [The canvas](/guides/the-canvas#overflow) has the rest.

:::warning
The ring is development only. Run `deckard check-overflow` before you present, since a clipped slide looks fine right up until the room sees it.
:::

## Slide "…" rendered no [data-slide-frame] element

```text
Slide roadmap rendered no [data-slide-frame] element. Check that the route still renders SlideShell.
```

The overflow check found the page but not the canvas. `app/slides/[id]/page.tsx` has stopped re-exporting `createSlideRoute`. `deckard doctor` reports the same thing as a `routes` failure.

## Theme validation fails

`deckard validate` reads the stylesheet the deck actually renders, so these name either `@thebuilder/deckard-themes/<id>` or your ejected `deck/theme/theme.css`.

**The class is not in the stylesheet.**

```text
Theme "custom" sets className "custom-theme", but deck/theme/theme.css has no rule for .custom-theme. The canvas carries that class, so none of the theme reaches the slide.
```

The canvas gets the class from the theme object. If the stylesheet has no rule for it, the deck renders in app defaults. Check for a typo on one side or the other.

**A dark mode the stylesheet never paints.**

```text
Theme "custom" lists "dark" in colorModes, but deck/theme/theme.css has no dark block. The toggle switches to a mode the theme never paints.
```

**A dark block the theme does not list.**

```text
deck/theme/theme.css has a dark block, but theme "custom" does not list "dark" in colorModes.
```

Add `"dark"` to `colorModes`, or delete the block.

**A token defined only in dark.**

```text
deck/theme/theme.css defines --slide-surface-border only in the dark block. Light mode renders without it.
```

Every token needs a light value. Dark overrides a subset.

**The default names a mode the theme does not carry.** These come from the theme object rather than the stylesheet:

```text
Slide theme "custom" defaults to "system" but only supports dark. Support both color modes or pick one as the default.
```

:::note
Nothing validates that a theme defines any particular token. A theme missing `--slide-heading-size` passes `deckard validate` and renders at the inherited size. Read the ejected `THEME.md` for the set the theme was written against.
:::

## The presenter window does not open

**Symptom.** `P` or the popout button does nothing at all. No error, no console message.

**Cause.** The browser blocked the popup. `window.open` returned nothing and the code does not report it.

**Fix.** Allow popups for the deck's origin, then press `P` again.

Also check the other two reasons the key does nothing: `P` is ignored while focus is in an input, textarea, select, or `contenteditable`, and while any modifier is held. And a slide loaded with `?presenterPreview=1` is read-only by design, so its keys do not fire.

## The presenter window does not sync

**Symptom.** The presenter window opens and stays on `Waiting for connection from the slideshow tab.`

**Cause.** The two windows talk over a `BroadcastChannel` named `slideshow-presenter-sync`, and that channel is same-origin and same-browser. Nothing routes through a server.

So it will not connect when:

- The deck and the presenter window are on different origins, including `localhost:3000` against `127.0.0.1:3000`, and http against https.
- They are in different browsers, or one is in a private window.
- The browser has no `BroadcastChannel`. The runtime checks for it and carries on without sync rather than failing.

**Fix.** Open the presenter window from the deck itself, with `P` or the popout button, so it inherits the origin. It reads `Connected via BroadcastChannel.` once the first message lands.

A presenter window opened by pasting the URL into a different browser will never connect, and that is the design rather than a fault.

## PDF export cannot find Chromium

**Symptom.**

```text
Failed to launch Chromium. Run: pnpm exec playwright install chromium
```

**Cause.** The Playwright package is installed in the deck, but the browser binary is a separate download.

**Fix.** Run the line the message printed. It names your deck's package manager, so an npm deck is told `npm exec -- playwright install chromium` and a bun deck `bunx playwright install chromium`.

This hits `check-overflow`, `screenshots`, `contact-sheet`, and `export pdf`. It never hits `validate`, `doctor`, `init`, `add`, or `eject`, which drive no browser.

## A capture command cannot find playwright or pdf-lib

**Symptom.**

```text
playwright does not resolve from this deck. The capture commands load it from the deck's own node_modules. Install it: pnpm add -D playwright
```

**Cause.** The CLI does not ship the browser or the PDF writer. `deckard init` writes both into the deck's `devDependencies`; a deck added to an existing app by hand, or one that removed them, has neither.

**Fix.** Run the line the message printed, then the Chromium download above. `deckard doctor` lists both packages under `tooling`.

### The port is taken

```text
Something is already serving http://localhost:3410. Stop it, or pass --port=<free port>.
```

The capture commands refuse to run against a server they did not start, because it might be serving a different build. Stop it or move.

### No screenshots to compose

```text
No screenshots at /path/to/deck/out/screenshots. Run: pnpm run deck:screenshots
```

`contact-sheet` reads the manifest that `screenshots` writes and builds nothing itself.

### The sitemap has no slides

```text
No slide routes in the sitemap. Check that app/sitemap.ts still lists /slides/* entries.
```

The capture commands read the slide list from the deck's own `/sitemap.xml`. An app that dropped or rewrote that route breaks all three.

## The registry cannot be reached

**Symptom.**

```text
The registry at https://deckard.thebuilder.dk/r/metrics.json did not answer.
```

**Cause.** The URL in `components.json` points at the Deckard docs site, which serves every registry item at `/r/{name}.json`. Either the site is unreachable or the URL was changed.

**Fix.** Check the network, or point `deckard add block` somewhere else with `--registry <url>`. The URL carries a `{name}` placeholder.

**A different message, no registry configured at all:**

```text
No @deckard registry in components.json and no --registry flag.
Add one: { "registries": { "@deckard": "https://deckard.thebuilder.dk/r/{name}.json" } }
```

The blocks are yours as source either way. `deckard add block` is for taking an update or undoing an edit, so a deck that never needs one never needs the registry. [Blocks and the registry](/guides/blocks-and-the-registry) has the details.

## Styles are missing

**Symptom.** The deck renders as unstyled text, or the layout is right and every colour is wrong.

**Cause and fix.** `deckard doctor` names it:

```text
FAIL styles  app/globals.css never imports @import "@thebuilder/deckard-core/styles.css"
             Add @import "@thebuilder/deckard-core/styles.css"; to app/globals.css. It carries the --slide-* tokens and registers the runtime's own Tailwind source.
```

The check is a literal substring test, so the import has to read exactly `@import "@thebuilder/deckard-core/styles.css"`.

If the import is there and the deck is still unstyled, the theme class is the next thing to check. `deckard validate` prints the stylesheet it read and the token counts:

```text
theme  meridian (.meridian-theme): light and dark, default system
       57 tokens in the light block, 25 dark overrides, from @thebuilder/deckard-themes/meridian
```

Zero tokens, or a stylesheet path you did not expect, is the answer.

## Other messages

**A slide that throws under `next dev`** renders an inline card reading `Slide <id> threw while rendering`, and the rest of the deck keeps working. In a production build the same throw is fatal to the route and Next serves its own error page, so fix it before you build.

**`deckard <command> --help`** is an error rather than help. `--help` lives at the top level only.

```text
Unknown flag --help. This command takes: --light, --port, --skip-build.
```

**`deckard eject theme` run twice** refuses, because there is a local theme it will not overwrite. It is a one-way door.

**`node` fails in `doctor`:**

```text
Next 16 needs node 20.9.0 or newer.
```

[Requirements](/reference/requirements) has the full version table.
