Writing a theme
A theme is a look: CSS scoped under .theme-<name> that sets the deck’s tokens and styles its
layouts, plus, if it wants, layouts, components and layers of its own. An addon is a feature any
theme can use. Both are npm packages (or plain folders) of the same shape. To change an existing
theme for one deck, eject it instead.
1. Start from the scaffold
Section titled “1. Start from the scaffold”npx kadal-deck new --theme auroracd kadal-theme-auroranpm installnpm run dev # the preview deck, livepnpm exec kadal-deck new --theme auroracd kadal-theme-aurorapnpm installpnpm dev # the preview deck, liveyarn kadal-deck new --theme auroracd kadal-theme-aurorayarn installyarn dev # the preview deck, livebunx kadal-deck new --theme auroracd kadal-theme-aurorabun installbun run dev # the preview deck, live(npm create @kadal/deck -- --theme aurorapnpm create @kadal/deck -- --theme aurorayarn create @kadal/deck -- --theme aurorabun create @kadal/deck -- --theme aurora does the same without the CLI installed.) You get a
package that already follows every rule on this page:
| File | What it is |
|---|---|
package.json |
the name kadal-theme-aurora, the kadal-theme keyword and the theme’s metadata |
style.css |
every token in light and dark, every layout class styled |
layouts.tsx |
a layout of its own, hero |
index.tsx |
exports the layouts, and a footer layer that themeConfig.footer: false hides |
preview/ |
a deck that uses the theme from its folder (theme: ../): npm run devpnpm devyarn devbun run dev, npm run checkpnpm checkyarn checkbun run check and npm run buildpnpm buildyarn buildbun run build run it |
README.md |
install, themeConfig and layouts, ready to fill in |
tsconfig.json |
types for the module and the preview |
2. The metadata
Section titled “2. The metadata”package.json carries what the engine needs to know, under kadal.theme:
{ "name": "kadal-theme-aurora", "keywords": ["kadal-theme"], "type": "module", "exports": { ".": "./index.tsx", "./style.css": "./style.css", "./package.json": "./package.json" }, "kadal": { "theme": { "name": "aurora", "title": "Aurora", "colorScheme": "both", "themeConfig": { "primary": "#0f766e", "footer": true }, "config": { "transition": "fade", "fonts": { "sans": "Inter", "heading": "Fraunces", "mono": "JetBrains Mono" } }, "defaults": {}, "shiki": { "light": "github-light", "dark": "github-dark" }, "layouts": ["hero"], "components": [] } }, "peerDependencies": { "@kadal/deck": "^0.4.2", "react": "^19.2.0" }}nameis the CSS class (.theme-aurora) and what decks write astheme:.colorScheme:both,lightordark. A one-scheme theme makes that scheme the deck’s default.configis headmatter the theme brings (a deck’s own headmatter wins);defaultsis frontmatter for every slide (a deck’sdefaultsand a slide’s own keys win).themeConfigholds the defaults of the theme’s knobs (below);shikicolours the code.layoutsandcomponentsname what the module adds, forkadal-deck check,info themesand AI agents.
./style.css is the only required export. TSX source is fine for .: decks compile it.
3. The stylesheet
Section titled “3. The stylesheet”Scope everything under .theme-<name> (other themes sit on the same page) and set every token, in
light and in dark:
.theme-aurora { --deck-bg: #fbfaf7; --deck-fg: #1c1917; --deck-muted: #57534e; --deck-accent: var(--theme-primary, #0f766e); /* … every token below … */}.theme-aurora[data-color-scheme="dark"] { --deck-bg: #0c0a09; --deck-fg: #f5f5f4; /* … */}Write the dark block exactly as .theme-<name>[data-color-scheme="dark"], never .dark .theme-… or a
media query: a slide that forces its own scheme (colorScheme: dark) carries the attribute too.
| Token | For |
|---|---|
--deck-bg, --deck-fg |
the slide’s background and text |
--deck-muted |
subtitles, captions, the slide number |
--deck-accent, --deck-accent-2 |
links, highlights, the progress bar; a second accent |
--deck-surface, --deck-border |
cards, callouts and inline code; hairlines |
--deck-heading-font, --deck-body-font, --deck-mono-font, --deck-serif-font |
the faces |
--deck-code-bg, --deck-code-fg, --deck-code-active |
code blocks and the highlighted line |
--deck-radius, --deck-pad, --deck-base-size |
corners, the slide’s padding, the base font size (slides are 1920 × 1080) |
--deck-live, --deck-transition, --deck-controls-color |
the live room’s accent, the transition’s length, the arrows |
Then style the layouts: .deck-layout-default, -center, -cover, -intro, -section, -end,
-fact, -quote, -statement, -full, -two-cols, -two-cols-header, -image, -image-left,
-image-right, -iframe, -iframe-left, -iframe-right. Every slide is
section.deck-slide.layout-<name> with the layout’s .deck-layout inside, so content is
.theme-aurora .deck-slide h1, … blockquote, … table. Slots are .deck-slot.deck-slot-<name>.
Classes themes often touch: .deck-progress span, .deck-slide-number, .deck-callout,
.deck-badge, .deck-code-block, .deck-code-title, .deck-kbd, .deck-toc. Prefix your own classes
with the theme’s name.
Decoration. Shapes that reach past the slide’s padding (a ground, a wave along the foot, a corner
blob) go on the slide (.theme-aurora .deck-slide.layout-cover::before) or on an element with the
deck-absolute class, never off .deck-layout. Anything that sticks out of .deck-layout counts as
overflow, and the engine’s auto-fit shrinks the whole slide to make room for it.
4. Settings: themeConfig
Section titled “4. Settings: themeConfig”Each themeConfig key, the theme’s defaults with the deck’s values over them, arrives on the deck root
as --theme-<kebab-key>: primary → --theme-primary, primaryColor → --theme-primary-color,
cover: { image } → --theme-cover-image. Lists become comma lists, booleans 1 or 0. Always read
them with a fallback:
.theme-aurora { --deck-accent: var(--theme-primary, #0f766e); }A layout or a layer reads the raw values from useDeck().config.themeConfig. Document every key in
the README.
5. Fonts
Section titled “5. Fonts”Name the families in the tokens, and load them through the metadata’s config.fonts
({ sans, heading, serif, mono, weights, italic, provider, local, fallbacks }) rather than an @import.
Exports wait for fonts loaded this way, and a theme picked from the theme menu brings them along.
A deck’s own fonts: wins over the theme’s.
6. Layouts, components and layers
Section titled “6. Layouts, components and layers”The module is optional. Its default export (named exports work too) adds to the deck:
import type { LayerProps, LayoutProps, SetupApi } from "@kadal/deck";import { useDeck } from "@kadal/deck";
function Hero({ children }: LayoutProps) { return <div className="deck-layout deck-layout-hero">{children}</div>;}
function Footer({ page, total, layout }: LayerProps) { const deck = useDeck(); if (layout === "cover" || deck.config.themeConfig.footer === false) return null; return ( <footer className="aurora-footer"> {deck.config.title} · {page} / {total} </footer> );}
export default { layouts: { hero: Hero }, // layout: hero; a built-in name replaces that layout components: {}, // usable in any slide by name layers: { "slide-bottom": Footer }, // also global-top, global-bottom, slide-top, custom-nav-controls routes: [], // pages of the theme's own, e.g. { path: "handout", component: Handout } setup(api: SetupApi) { // optional; may return a cleanup api.shortcut("shift+k", () => api.deck.actions.toast("hi")); },};A layout wraps its children in .deck-layout plus a class of its own, and receives frontmatter and
options. A layer gets the slide, its page number, the click and print (true on a printed page or
a thumbnail), so read props.slide, not the navigation. routes adds pages under the deck’s
address, as a deck’s own routes.tsx does (see Layers, controls and pages).
setup(api) can register commands, presenter sidebar panels, backgrounds, island renderers,
shortcuts and pages (api.addRoute).
The deck’s own components and layouts win over an addon’s, which win over the theme’s. Layers stack: the theme’s lowest, then the addons’, then the deck’s.
7. Test it
Section titled “7. Test it”npm run devpnpm devyarn devbun run devruns the preview deck;npm run checkpnpm checkyarn checkbun run checklints it;npm run buildpnpm buildyarn buildbun run buildbuilds it.- Look at it in light and dark, every layout (
two-cols-headertoo), the presenter view, print (?print-pdf) and a phone-width scroll view (?view=scroll). - Check the contrast of
--deck-mutedon--deck-bg, and keep anything that moves still underprefers-reduced-motion. - From a deck:
npx kadal-deck info themespnpm exec kadal-deck info themesyarn kadal-deck info themesbunx kadal-deck info themesshould list it with its layouts.
Contributing a theme to the Kadal Deck repository (packages/theme-<name>)? Two more tools run there:
bun tools/theme-lint.ts packages/theme-<name> checks the contract statically (each theme’s
bun test runs it), and node tools/theme-shots.mjs <name> runs the preview deck in Chromium: a PNG
of each slide in light and dark, the WCAG contrast of the resolved tokens, anything that overflows or
that auto-fit had to shrink, the presenter, print and phone views, and contact sheets. A first-party
theme needs no build step and no edit anywhere else: the hosted player finds every packages/theme-*
on its own.
8. Publish
Section titled “8. Publish”npm publishKeep the kadal-theme keyword: it is how people, and kadal-deck info themes, find it. A deck then
uses it with npm i kadal-theme-aurorapnpm add kadal-theme-aurorayarn add kadal-theme-aurorabun add kadal-theme-aurora and theme: aurora.
Addons
Section titled “Addons”npx kadal-deck new --addon <name>pnpm exec kadal-deck new --addon <name>yarn kadal-deck new --addon <name>bunx kadal-deck new --addon <name> scaffolds an addon the same way. Its metadata sits under
kadal.addon (name, title, description, defaults, layouts, components), its keyword is
kadal-addon, and its module has the same layouts, components, layers, routes and setup.
A deck lists it with addons: [name], which finds @kadal/addon-<name>, then kadal-addon-<name>,
then a package of that name or a folder.