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: fascicolo → themes/fascicolo.css). To add one:
- Copy
default.csstofrontend/src/styles/themes/my-theme.css. - Re-tune the
--vd-*values. Don't invent new variable names without updating every theme — components assume the full set is defined. - 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:
- A built-in theme — the name matches a file in
frontend/src/styles/themes/(operazione-stile,fascicolo,default, plus any the instance has added). - 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.cssserves the raw CSS). - 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:
- Must define at least one
--vd-*custom property (otherwise it isn't a theme). - Maximum size: 64 KB.
- Rejected outright: any
<character (keeps HTML/</style>breakouts out),@import,javascript:URLs,expression(),behavior:,-moz-binding. url(...)may only reference relative paths ordata:image/*— externalhttp(s)fetches are blocked.
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
- Start from
default.css— it's the complete contract with comments. - Check both modes. Flip the dark/light toggle before publishing; if you only tuned dark tokens, light mode falls back to your dark values.
- Use the sandbox. Paste a scratch deck at
/sandboxto preview content quickly; for theme work, the builder's live preview shows every card type. - Keep contrast honest.
--vd-texton--vd-card-bgis the pairing readers stare at longest.