Themes
The built-in presets, how a deck imports one, what ejecting gives you, and what it takes to write your own.
A theme owns every audience-facing color, size, and background in the deck, including the header and the footer. Its stylesheet is scoped to a class that SlideCanvas puts on the canvas element, so the deck controls, command center, and presenter console keep the app tokens and stay readable whatever the deck looks like.
A theme is three files:
| File | What it holds |
|---|---|
theme.css |
Every token and every selector, scoped to the theme class. |
index.ts |
One SlideTheme object. |
THEME.md |
What each token controls and what this theme sets it to. |
The built-ins
Each frame is a slide canvas, not a picture of one, painted by that theme’s own stylesheet in the color mode the theme opens on. Follow one into the theme gallery for the same frames at a size you can read, with a light and dark switch on every card and the palette behind it.
| Theme | defaultColorMode | The idea |
|---|---|---|
meridian | system | The quietest one. Flat surfaces, no shadow in either mode. |
atelier | system | A print poster. Hard rules, flat colour blocks, no radius. |
aurora | dark | A live field behind the openers and the close, frozen to capture. |
blueprint | dark | A drafting sheet. A ruled field, boxed numerals, hairline gutters. |
cotton | system | Large radii, one soft shadow, and tints instead of lines. |
ledger | system | A bound report. Serif, sans, and mono, each with one job. |
nexus | dark | A flight console. Capitalised headings with an accent halo. |
noir | dark | A lookbook. Hairlines, centred layouts, and no mono face. |
phosphor | dark | A green CRT. Monospace everywhere, scanlines, a heading bloom. |
quorum | light | A board pack. A tight scale, serif figures, no boxed cards. |
Picking one
Import it and hand it to defineDeck. Importing the module pulls in its stylesheet:
import { defineDeck } from "@thebuilder/deckard-core"
import { phosphor } from "@thebuilder/deckard-themes"
import { slides } from "@/deck/slides"
export const deck = defineDeck({ slides, theme: phosphor, /* ... */ })
deckard add theme <name> makes that edit for you, rewriting both the import and the theme property.
Only the theme a deck imports reaches the bundle. The barrel re-exports every built-in and the bundler drops the modules nothing uses, stylesheets included.
The stylesheet on its own
@thebuilder/deckard-themes/<name>/theme.css is the same file the module imports, exported as a subpath so something that is not a deck can load it:
import "@thebuilder/deckard-themes/ledger/theme.css"
A deck never needs this. It exists for a page that renders a slide outside the runtime, such as the theme gallery on this site, which builds a scaled-down canvas out of the real markup and lets each theme’s own stylesheet paint it.
Ejecting
deckard eject theme copies the theme the deck imports out of the installed package into deck/theme/ as theme.css, index.ts, and THEME.md, then repoints deck/deck.ts at the copy. From then on the theme is your source and the package never touches it.
deckard eject theme
deckard eject theme --theme ledger # eject a different built-in instead
It refuses when deck/theme/ already exists.
The SlideTheme object
import type { SlideTheme } from "@thebuilder/deckard-core"
import "./theme.css"
export const theme = {
className: "my-theme",
colorModes: ["light", "dark"],
defaultColorMode: "system",
id: "my-theme",
} satisfies SlideTheme
| Field | Type | What it does |
|---|---|---|
className |
string |
The class SlideCanvas puts on the canvas. Every selector in theme.css is scoped to it. |
colorModes |
("light" | "dark")[] |
Which modes the stylesheet actually carries. |
defaultColorMode |
"light" | "dark" | "system" |
What a browser opens on. |
id |
string |
Names the theme in deckard validate and deckard eject theme. |
defineDeck validates this. defaultColorMode: "system" needs both modes in colorModes. A theme listing one mode pins the canvas to it and hides the light/dark toggle.
The token contract
@thebuilder/deckard-core/styles.css declares every --slide-* token with a neutral default, in the base layer. A theme redefines them on the canvas and wins, because it ships unlayered CSS. The runtime depends on the tokens, never on a preset, so a deck with no theme still renders in neutral sizes with no decoration.
The token reference lists all of them. The short version of what a theme sets:
- Type: the font families, the size scale, and the label tracking.
- Surfaces: the two radii, a surface color, a muted surface, a border, a shadow.
- Backgrounds: a grid color and pitch, a rule color, and the
--slide-scanline,--slide-hatchand--slide-haloeffects, which thebackgroundvariants paint with. - Chrome: the colours and type of the header and footer, plus
--slide-header-spaceand--slide-footer-spacefor how much canvas each gets. - Media: a foreground pair and the overlay gradients for text over photography.
- Figures:
--slide-figure-size, its unit size, and the height of aStatGridmeter.
The other half of the contract
Tokens are values. Data attributes are parts, and they are the rest of the contract.
Every part of a block a theme might want to reach carries one, from data-slide-eyebrow and data-slide-list-marker to data-stat-meter. A theme styles those attributes, decorative ::before and ::after content included, which is how phosphor writes [x] where a list number goes and ledger hangs a rule there instead, neither of them editing the block. The full list is also the “Block part contract” comment at the bottom of @thebuilder/deckard-core/styles.css, and a block may rewrite its markup and its class names freely as long as those attributes stay put.
A theme styles parts. It does not add content. A boot log, a status table, a cursor line with words in it are slide content, so they belong in a block the deck composes, however much they look like decoration.
Background variants
A slide picks one with background:
| Value | What the theme paints |
|---|---|
"default" |
The theme’s standard slide background. |
"spotlight" |
A second texture, usually a hatch, behind an opener. |
"grid" |
A ruled or dotted field. |
"accent" |
The inverted statement slide. |
"none" |
Nothing painted. |
A theme may name variants of its own. A deck writes the name as background, the theme styles [data-slide-background="<name>"], and deckard validate reports a name neither the runtime nor the theme paints. A theme paints one of its own variants with an animated canvas instead of CSS by naming it in motion, which is in the core reference.
SlideBackground renders one element carrying data-slide-background, and the canvas carries the variant name too, so a theme can restyle the chrome along with the sheet. What each variant paints lives entirely in theme.css, so changing the look is a theme edit. Read the theme’s THEME.md first, and run deckard eject theme if it is still an import.
"accent" floods the canvas with the theme accent and flips the ink, chrome included. Use it once or twice in a deck, for the one sentence that has to land, and never on two slides in a row.
No variant paints a radial wash or a corner glow. The textures a theme has are flat colour, a rule, a grid, and a diagonal hatch.
Writing your own
Start by ejecting the built-in closest to what you want. Writing theme.css from scratch means rediscovering which tokens the blocks actually read, and the THEME.md that comes with an ejected theme is that list.
Then:
Rename the class and the id
Change className and id in index.ts, and the selector prefix
throughout theme.css. They do not have to match; every built-in uses
<id>-theme as the class.
Set the tokens
Work top-down: the shadcn color variables first (--background, --card,
--primary, --border, --foreground, --muted), then the --slide-*
layer on top. The blocks read the slide tokens, so most visual changes are
one line.
Do both color modes
Or declare only one in colorModes and pin the deck to it. deckard validate checks that the dark block defines nothing the light block does
not, and that colorModes matches the blocks the stylesheet carries.
Style the chrome
The header and footer are yours. Core renders [data-slide-header] and
[data-slide-footer] with named parts for the brand, title, meta line,
counter, and progress element. See Deck chrome.
Validate
deckard validateIt checks the theme class is a real selector in the stylesheet the deck renders, and names which file it read.
Style inside the canvas with semantic tokens (bg-card, text-muted-foreground) or slide tokens. Never a hardcoded color, and never a raw font size where a token exists.