@thebuilder/deckard-core
Every entry point the package exposes, what each one exports, and the Next.js route adapters an app re-exports.
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 route adapters |
@thebuilder/deckard-core/share-card |
createSlideShareCard, kept off /next because next/og loads a WebAssembly renderer |
@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. |
deckSiteUrl(siteUrl?) |
The deck’s public origin: siteUrl, else NEXT_PUBLIC_SITE_URL, else the production domain on a Vercel production build or the branch address on a preview, else http://localhost:3000. The sitemap and the share card URLs are built on it. |
toDeckPresentation(deck) |
The serializable slice the shell needs. |
canSwitchColorMode(theme) / forcedColorMode(theme) |
What the color-mode toggle does for a given theme. |
homeColorMode(theme) |
The mode a theme is shown in when only one can be: its defaultColorMode, or light for "system". The share card paints it. |
cardFontWeights |
The weights a share card face can be set in, 100 to 900. |
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. |
resolveBackground(theme, background) |
Returns { variant, field? }: the name the canvas carries and the field it paints, with a role the theme does not paint in motion replaced by its fallback. |
slideBackgroundModes |
The background variants the runtime paints for every deck. |
slideBackgroundRoles |
The background roles, each mapped to the variant it renders as when the theme paints no field for it. |
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, SlideThemeCard, SlideThemeCardColors, CardFontWeight, SlideColorMode, SlideHeaderMode, SlideFooterMode, SlideFooterModeInput, SlideLayoutMode, SlideBackgroundMode, BuiltInSlideBackgroundMode, PaintedSlideBackgroundMode, SlideBackgroundRole, ResolvedBackground, 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. Key it by a background role, hero, breaker, statement, or closing, so a deck that marks its slides by role shows the field under this theme and the role’s fallback under any other:
export const aurora: SlideTheme = {
className: "aurora-theme",
colorModes: ["dark", "light"],
defaultColorMode: "dark",
id: "aurora",
motion: { closing: "waves", hero: "aurora", statement: "wash" },
}
{ background: "hero", body: <HeroSlide … />, slug: "opening" }
The 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. These 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 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.
Where no canvas mounts
The canvas mounts once the slide has read its URL, and never in a presenter preview, the page the console’s iframes load with presenterPreview=1. Until then, and in every preview, the background the theme painted underneath is the slide and the runtime is not fetched.
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 |
createSlideShareCard(deck) |
app/slides/[id]/opengraph-image.tsx |
Image, alt, contentType, generateStaticParams, size |
import { createSlideRoute } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"
const { Page, generateMetadata, generateStaticParams } = createSlideRoute(deck)
export { generateMetadata, generateStaticParams }
export default Pageimport { createPresenterPage } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"
const { Page, metadata } = createPresenterPage(deck)
export { metadata }
export default Pageimport { createDeckSitemap } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"
export default createDeckSitemap(deck)import { createFirstSlideRedirect } from "@thebuilder/deckard-core/next"
import { deck } from "@/deck/deck"
export default createFirstSlideRedirect(deck)import { createSlideShareCard } from "@thebuilder/deckard-core/share-card"
import { deck } from "@/deck/deck"
const { Image, alt, contentType, generateStaticParams, size } =
createSlideShareCard(deck)
export { alt, contentType, generateStaticParams, size }
export default ImageEvery 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 takes its origin from deckSiteUrl when you pass no siteUrl: NEXT_PUBLIC_SITE_URL, then the address Vercel gives the deployment, then http://localhost:3000. createSlideRoute’s generateMetadata sets metadataBase from the same function, so the share card each slide links resolves to an absolute URL on the same origin. 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.
createSlideShareCard
import { createSlideShareCard } from "@thebuilder/deckard-core/share-card"
function createSlideShareCard(deck: Deck): {
Image: (props: { params: Promise<{ id: string }> }) => Promise<Response>
alt: string
contentType: string
generateStaticParams: () => { id: string }[]
size: { height: number; width: number }
}
| Argument | Type | Default |
|---|---|---|
deck |
Deck, the result of defineDeck |
required |
Returns the exports of an opengraph-image.tsx route beside the slide page:
| Export | What it is |
|---|---|
Image |
The default export. Renders one slide’s card with ImageResponse from next/og. |
alt |
A slide from <deck title>, with by <brand> when header.brand differs from the title. Every card in the deck shares it. |
contentType |
"image/png" |
generateStaticParams |
Every slide id, the same list the slide page prerenders. |
size |
{ width: 1200, height: 630 } |
Behaviour. next build prerenders one PNG per slide at <slides path>/<id>/opengraph-image, and each slide page links it as og:image and twitter:image, with twitter:card set to summary_large_image. The card paints the theme’s card.colors for its home mode, homeColorMode(theme): the brand and the slide position (3 / 18) along the top in muted, an accent rule, the slide title in foreground, and deck.description along the bottom in muted, or header.meta when the description is empty. The title steps down in size as it gets longer and clamps at three lines. It is set in card.font.family at card.font.weight, capitalised when card.font.uppercase is true; the eyebrow and footer use the same family at 400.
The face is fetched at build time from the Google Fonts CSS2 API, subset to the characters on the card and cached per family, weight, and text for the build. A theme with no card renders a grayscale card in the renderer’s default face and fetches nothing.
The route reads no request, so it prerenders with the slides. Re-export generateStaticParams from the image route as well: it is a route handler and does not take the slide page’s params, so without it no card prerenders.
Errors. A font fetch that fails, because the build is offline or Google Fonts does not carry the family, logs one warning naming the family and weight, and the card renders in the default face. The build does not fail. An id that names no slide answers 404. A theme card with a color that is not hex or rgb() fails defineDeck, naming the theme, the mode, and the field.
Example. The playground’s second deck lives at /example, so its card route sits beside that page:
import { createSlideShareCard } from "@thebuilder/deckard-core/share-card"
import { exampleDeck } from "@/deck/example-deck"
const { Image, alt, contentType, generateStaticParams, size } =
createSlideShareCard(exampleDeck)
export { alt, contentType, generateStaticParams, size }
export default Image
Related. SlideThemeCard for the theme side, Share cards for the site URL and the font fetch, deckSiteUrl for the origin.
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.