Theming Vibedeck

How themes work, every token in the contract, and how to build your own — whether you're a signed-in author on a hosted instance or a self-hoster adding themes to the codebase.

For the in-product version of this guide, see the bundled Crafting a VibeDeck Theme deck (samples/vibedeck__crafting-a-vibedeck-theme.md).


What a theme is

Vibedeck components never hardcode visual values — every colour, font, and shape reads a --vd-* CSS custom property. A theme is anything that defines that set of tokens on :root. Define the tokens once and every card type, the reader chrome, and the navigation restyle themselves.

The canonical token list lives in frontend/src/styles/themes/default.css. Visiting /themes/default.css on any instance downloads a copy to start from.


Two ways to create a theme

1. The theme builder (signed-in authors — no code)

Open /account/theme. The builder presents every token as a form control (colour pickers, font fields, sliders), previews the result live, and saves the generated CSS under a name you choose. Because the form only emits constrained token values, the result is safe by construction.

Use it in a deck by setting the theme name in frontmatter:

theme: my-theme

Your theme renders for every reader of that deck — it's inlined server-side, not just applied to your own view.

To iterate on an existing theme, the builder can load any of your saved themes back into the form.

2. A CSS file in the repo (self-hosters)

Built-in themes are plain CSS files in frontend/src/styles/themes/ — one file per theme, where the frontmatter name maps to the filename (theme: fascicolothemes/fascicolo.css). To add one:

  1. Copy default.css to frontend/src/styles/themes/my-theme.css.
  2. Re-tune the --vd-* values. Don't invent new variable names without updating every theme — components assume the full set is defined.
  3. Rebuild the frontend (cd frontend && npm run build). Themes are resolved by a build-time glob, so a new file is picked up automatically — no registration step.

File-based themes can go beyond tokens (see Restyling components).


How a deck's theme resolves

The theme frontmatter field resolves in this order:

  1. A built-in theme — the name matches a file in frontend/src/styles/themes/ (operazione-stile, fascicolo, default, plus any the instance has added).
  2. The author's custom theme — no built-in matches, so the deck's owner's saved themes are checked for that slug. A match is inlined into the page at render time for all readers (GET /api/decks/{topic}/{deck}/theme.css serves the raw CSS).
  3. Fallback — no match anywhere: the deck renders with default.

Topics can also carry a theme, applied to their index pages the same way.


The token reference

Defaults shown are from default.css (the dark baseline).

Surfaces

Token Controls Default
--vd-bg Page/stage background #0f172a
--vd-card-bg Card and panel surfaces #1e293b
--vd-card-border Card, panel, and divider borders #334155

Text

Token Controls Default
--vd-text Body text #e2e8f0
--vd-text-muted Secondary text, captions, hints #94a3b8
--vd-heading Headings and the title card #f8fafc

Accent

Token Controls Default
--vd-accent Links, active states, quote rules, primary buttons #818cf8
--vd-accent-2 Gradient partner / secondary accent #c084fc

Typography

Token Controls Default
--vd-font-body Body font stack system-ui stack
--vd-font-heading Heading font stack inherits --vd-font-body
--vd-font-scale Master type-size multiplier for the theme 1

Layout

Token Controls Default
--vd-card-max-width Card column width 40rem
--vd-card-padding Inner card padding clamp(1.5rem, 5vw, 3rem)
--vd-radius Corner radius for cards, buttons, inputs 1rem

Navigation chrome

Token Controls Default
--vd-nav-bg Reader nav bar background (translucent works well) rgba(15, 23, 42, 0.8)
--vd-progress "Page n / total" indicator inherits --vd-text-muted

Light and dark mode

The site-wide toggle (OS-aware) sets data-mode on the page's root element. A theme supports the other mode by overriding tokens under a matching selector — define only what changes:

:root[data-mode="light"] {
  --vd-bg: #f1ecdd;
  --vd-card-bg: #faf6ea;
  --vd-text: #2e231b;
  --vd-heading: #1a120c;
}

A theme with no data-mode block simply looks the same in both modes.


Restyling components (CSS-file themes only)

A file-based theme can restyle component internals, not just recolour them. Target a card's body with an ancestor chain specific enough to beat the components' scoped styles:

.reader__stage .reader__card .card-body--quote {
  border-left: 4px solid var(--vd-accent);
  font-style: italic;
}

This is how the bespoke built-in themes add stamps, rules, and textures — see operazione-stile.css for a worked example.

The theme builder deliberately stays in token territory; uploaded theme CSS is validated (below), so selector-based rules belong in repo files.


Validation rules (custom themes)

CSS submitted through the API (POST /api/themes — which is what the builder calls) is validated by backend/services/themes.py:

On the server edition, non-admin users can save up to QUOTA_MAX_THEMES themes (default 20).


API quick reference

Endpoint What it does
POST /api/themes Save a theme ({name, css}) — validated as above
GET /api/themes/mine List your themes
GET /api/themes/mine/{slug}.css Raw CSS of one of your themes
DELETE /api/themes/mine/{slug} Delete one of your themes
GET /api/decks/{topic}/{deck}/theme.css Public: the custom theme a deck uses (404 if it uses a built-in)

All mine routes require a session (Authorization: Bearer <JWT>).


Tips