Skip to content

Theming & brands

A brand is one typed token object (colors, fonts, a chart palette), and the whole deck (slides, code, plugins) derives its look from it. Nothing is hardcoded in slides: a slide uses text-primary, not text-blue-500, so it’s right under any brand.

The theme styles ship a few brands (liebstoeckel, nocturne, acme, sunset). Import the styles once in your entry and pick one by name:

main.tsx
import "@liebstoeckel/theme/styles.css";
import { Present } from "@liebstoeckel/engine";
<Present brands={["nocturne"]} slides={[…]} />

Author it with defineTheme and hand it to <Present> via brandThemes. The engine injects it as a [data-brand] stylesheet, so you ship a brand without touching the theme package:

main.tsx
import "@liebstoeckel/theme/styles.css";
import { Present } from "@liebstoeckel/engine";
import { defineTheme } from "@liebstoeckel/theme";
const acme = defineTheme({
name: "acme",
colors: {
bg: "#0b0e14", surface: "#141925", border: "#222a3a",
text: "#e6eaf2", muted: "#8a93a6",
primary: "#3b82f6", // brand
accent: "#22d3ee", // accent
accent2: "#f0abfc", // optional secondary accent
onPrimary: "#ffffff", // text/icons on a primary fill
},
fonts: {
heading: '"Inter", system-ui, sans-serif',
body: '"Inter", system-ui, sans-serif',
mono: '"JetBrains Mono", ui-monospace, monospace',
},
viz: ["#3b82f6", "#22d3ee", "#f0abfc", "#a3e635", "#fbbf24"], // optional chart palette
glow: { a: "#10233f", b: "#0b2530" }, // optional atmosphere gradient
});
<Present brands={["acme"]} brandThemes={[acme]} slides={[…]} />

That’s it: brands names the active brand, brandThemes supplies its tokens. (Bring fonts via @font-face / a <link> in your index.html.)

CSS variableTailwindMeaningRequired
--brand-bg / --brand-surfacebg-bg / bg-surfacepage + panel backgrounds
--brand-text / --brand-mutedtext-text / text-mutedprimary + secondary text
--brand-primary / --brand-accenttext-primary / text-accentbrand + accent
--brand-on-primarytext-on-primarycontent on a primary fill
--brand-font-heading/body/monofont-heading/body/monotypefaces
--brand-borderborder-borderhairlines (falls back to a tint of text)optional
--brand-accent2text-accent2secondary accentoptional
--brand-viz-0…n(none)chart-series palette (theme.viz)optional
--brand-glow-a/b(none)atmosphere gradient stops (theme.glow)optional
  1. A brand is a [data-brand="name"] { --brand-*: … } block. Built-ins live in the theme styles; your brandThemes are injected as the same kind of block.

  2. @theme inline in the theme CSS maps Tailwind utilities onto those variables, so text-primary resolves to var(--brand-primary) at the point of use, under whichever data-brand is active.

  3. The deck sets document.body.dataset.brand, so the whole deck re-skins instantly, no rebuild.

Code and plugins inherit automatically: Shiki highlights against the brand tokens (see Animated code), and plugins style against var(--brand-*) (and read theme.viz for charts), so polls, Q&A, and reactions are on-brand for free.

To make a brand available to every deck by name (instead of per-deck brandThemes), add it to the theme package: drop a defineTheme(...) file in packages/theme/src/brands/, export it from brands/index.ts, and run bun run gen to regenerate brands.generated.css.

Comments

liebstoeckel

Code-first presentations your agent can author. One file out, no server.

© 2026 Leon Kaucher