---
title: "@thebuilder/deckard-core"
description: Every entry point the package exposes, what each one exports, and the four Next.js route adapters an app re-exports.
sidebar:
  label: Core API
  icon: box
---

The package compiles to `dist/` with `tsc`: ESM, `.d.ts`, and `"use client"` directives preserved. No bundler, so a stack trace still points at a file that matches the source. An app installs it and imports it like any other package, with no `transpilePackages` entry and no Tailwind `@source` of its own.

## Entry points

| Entry point | Contents |
| --- | --- |
| `@thebuilder/deckard-core` | `defineDeck`, slide resolution, summaries, theme helpers, every type |
| `@thebuilder/deckard-core/components` | The runtime components and slide context hooks |
| `@thebuilder/deckard-core/code-block` | `CodeBlock`, kept off the components barrel because shiki loads WebAssembly |
| `@thebuilder/deckard-core/discovery` | `discoverSlides` |
| `@thebuilder/deckard-core/next` | The four route adapters |
| `@thebuilder/deckard-core/slide-from-module` | `slideFromModule` |
| `@thebuilder/deckard-core/ui` | The shadcn primitives the runtime renders |
| `@thebuilder/deckard-core/utils` | `cn` |
| `@thebuilder/deckard-core/styles.css` | The `--slide-*` token contract |

`CodeBlock` is split out because shiki pulls in a WebAssembly regex engine. Importing it through the components barrel would turn every discovered slide module async, which discovery refuses. A slide that shows highlighted code belongs in the `slides` array.

## @thebuilder/deckard-core

| Export | What it does |
| --- | --- |
| `defineDeck(config)` | Resolve a `DeckConfig` into a `Deck`. |
| `resolveCanvas(partial)` | Fill in and validate the canvas config. |
| `resolveSlides(slides, defaults, slidesPath)` | Resolve a slide array. `slidesPath` defaults to `/slides`; `defineDeck` passes its resolved route. |
| `getSlideById(slides, id)` | Find a resolved slide by its id. |
| `normalizeFooterMode(mode)` | Collapse `"counter"` into `"visible"`. |
| `toSlideSummary(slide)` / `toSlideSummaries(slides)` | The serializable shape client components receive. |
| `resolveTheme(theme)` | Fill in and validate a `SlideTheme`. |
| `toDeckPresentation(deck)` | The serializable slice the shell needs. |
| `canSwitchColorMode(theme)` / `forcedColorMode(theme)` | What the color-mode toggle does for a given theme. |
| `isPdfExport()` / `pdfExportColorMode()` | Read the export environment variables. |
| `isCapturing()` | True while a capture tool is driving the page. |
| `captureAttribute` | The attribute a capture tool sets on the document element. |
| `motionField(theme, background)` | The field a theme paints a background variant with, or `undefined`. |
| `slideBackgroundModes` | The background variants the runtime paints for every deck. |
| `slideMotionFields` | The three fields a theme may name. |
| `PRESENTER_CHANNEL_NAME` | The `BroadcastChannel` name presenter mode uses. |

Types: `Deck`, `DeckConfig`, `DeckCanvasConfig`, `DeckHeaderConfig`, `DeckFooterConfig`, `DeckPresentation`, `ResolvedSlide`, `SlideDefinition`, `SlideMeta`, `SlideModule`, `SlideComponent`, `SlideDefaults`, `SlideSummary`, `SlideTheme`, `SlideColorMode`, `SlideHeaderMode`, `SlideFooterMode`, `SlideFooterModeInput`, `SlideLayoutMode`, `SlideBackgroundMode`, `BuiltInSlideBackgroundMode`, `SlideMotionMode`, `SlideMotionField`, `PresenterChannelMessage`, `PresenterPreviewState`, `PresenterSlideState`.

## @thebuilder/deckard-core/components

| Export | What it does |
| --- | --- |
| `SlideShell` | Assembles a slide: frame, canvas chrome, deck controls. |
| `SlideViewport` | Fits the canvas to the browser window and publishes `--deckard-scale`. |
| `SlideCanvas` | The fixed coordinate space slides are authored in. |
| `SlideCanvasHeader`, `SlideCanvasFooter` | The themed chrome inside the canvas. |
| `SlideBackground` | The element carrying `data-slide-background`, plus the motion canvas when the theme paints that variant with one. |
| `SlideMotionBackground` | The animated canvas itself. |
| `SlideScrollArea` | A scroll region that does not step the deck. |
| `SlideStepper`, `SlideStep`, `SlideStepAdvanceArea`, `useSlideStepper` | Step reveals. |
| `SlideCommandCenter` | The `Cmd/Ctrl + K` jump dialog. |
| `DeckControls` | The corner cluster. |
| `PresenterConsole`, `PresenterPopoutButton` | Presenter mode. |
| `ColorModeProvider`, `SlideshowColorModeToggle` | Light and dark. |
| `SlideContextProvider`, `useSlideTitle`, `useIsPresenterPreview` | Slide context. |
| `SlideErrorBoundary` | The inline error card a throwing slide renders under `next dev`. |
| `SlideMediaVideo` | A `<video>` that suppresses autoplay in the presenter preview. |
| `SlidePrefetch` | Prefetches the neighbouring slides. |
| `StaticMediaBoundary` | Freezes video and audio inside a slide the audience is not on, so a presenter preview never plays. |

### useSlideTitle

Returns the current slide's authored title, or `null`. Layout blocks call it so a slide's title stays defined in one place, and only take an explicit `title` prop when you want to override the text inside the layout.

### useIsPresenterPreview

Returns `true` inside the presenter console's preview iframe. Use it in a client component to skip autoplay, audio, canvas work, or anything else expensive.

### Motion backgrounds

A theme paints most background variants in CSS. It paints a variant in an animated canvas by naming it in `motion`, keyed by the name a deck writes as `background`:

```ts deck/theme.ts
export const aurora: SlideTheme = {
  className: "aurora-theme",
  colorModes: ["dark", "light"],
  defaultColorMode: "dark",
  id: "aurora",
  motion: { closing: "waves", hero: "aurora", statement: "wash" },
}
```

```ts deck/slides.tsx
{ background: "hero", body: <HeroSlide … />, slug: "opening" }
```

The three fields are `aurora`, banded curtains over noise; `waves`, a horizontal band that undulates; and `wash`, a soft noise field with a dither. Their colours and their speed come from the `--slide-motion-*` tokens, so the theme sets them per variant and per color mode in its stylesheet. A variant the theme never names is a CSS background like any other, and a deck whose theme names none never loads the WebGL runtime: it is fetched by the canvas component on mount.

One `requestAnimationFrame` loop drives every canvas on the page, and it skips a canvas that is off screen or has collapsed to nothing. Backing stores are half the canvas size in each axis.

#### Freezing

A frozen field still renders. It draws one frame at a fixed point in the animation and holds it, so two captures of the same slide are the same image. Five things freeze one:

| Reason | How it is decided |
| --- | --- |
| `prefers-reduced-motion: reduce` | `matchMedia`, live: dropping the preference starts the field |
| A capture is running | `isCapturing()`, which reads the PDF build flag and the `captureAttribute` marker the `deckard` capture commands set |
| The presenter preview | The `presenterPreview` query the console's iframes carry |
| The deck asked | `motion: "frozen"` in `defineDeck` |
| The slide asked | `motion: "frozen"` on the slide |

The canvas reports which of the two it is doing on `data-slide-motion-state`, valued `running` or `frozen`.

#### When there is no WebGL

The canvas paints nothing, reports `data-slide-motion-state="unavailable"`, and the background the theme painted underneath is the slide. The same holds while the runtime is loading and if the context is lost mid-presentation, so a theme paints a variant it animates as though it never would.

### useSlideStepper

Returns `{ currentStep, stepCount, advance, retreat, canAdvance, canRetreat, isReadOnly }`, or `null` outside a stepper.

## Route adapters

`@thebuilder/deckard-core/next` ships the route logic, so an app owns its deck and re-exports the routes. Each adapter takes the resolved deck and returns the route pieces.

| Adapter | File | Returns |
| --- | --- | --- |
| `createSlideRoute(deck)` | `app/slides/[id]/page.tsx` | `Page`, `generateMetadata`, `generateStaticParams` |
| `createPresenterPage(deck)` | `app/presenter/page.tsx` | `Page`, `metadata` |
| `createDeckSitemap(deck, { siteUrl })` | `app/sitemap.ts` | The sitemap function |
| `createFirstSlideRedirect(deck)` | `app/page.tsx` | `Page`, which redirects to the first slide |

<CodeGroup>

```tsx app/slides/[id]/page.tsx
import { createSlideRoute } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"

const { Page, generateMetadata, generateStaticParams } = createSlideRoute(deck)

export { generateMetadata, generateStaticParams }
export default Page
```

```tsx app/presenter/page.tsx
import { createPresenterPage } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"

const { Page, metadata } = createPresenterPage(deck)

export { metadata }
export default Page
```

```ts app/sitemap.ts
import { createDeckSitemap } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"

export default createDeckSitemap(deck)
```

```tsx app/page.tsx
import { createFirstSlideRedirect } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"

export default createFirstSlideRedirect(deck)
```

</CodeGroup>

Every slide prerenders statically. The route reads no request: `?presenterPreview=1` and `?step=` are read on the client, inside a Suspense boundary holding nothing but the reader.

`createDeckSitemap` falls back to `NEXT_PUBLIC_SITE_URL` and then to `http://localhost:3000` when you pass no `siteUrl`. That sitemap is also how `deckard screenshots`, `check-overflow`, and `export pdf` find the slide list, so an app that drops the route breaks those three.

The components the adapters compose are exported too, so a deck that needs its own route can write one.

## The dependency direction

`@thebuilder/deckard-core` holds nothing deck-specific. Colors, sizes, and backgrounds live in a theme, and a theme is data: the runtime depends on the token contract in `styles.css` and never on a preset.

`@thebuilder/deckard-themes` takes `SlideTheme` from core as a type-only import and names core a peer dependency, so nothing crosses at runtime and core does not know the themes exist.
