Manage themes with Vike config, defineTheme(), and useTheme(), then apply CSS variables before first paint.
Theme System
The theme system is built around one idea: compile theme tokens into a stable CSS variable contract, then apply that contract both before first paint and at runtime on document.documentElement.
Current data flow:
- Provide the default theme through
theme,themes, andappearancein+config.ts. vike-vue-content/configautomatically registersheadHtmlBeginand injects a small init script before first paint.- The init script reads
localStorage['vvc-theme']first; if there is no user override, it falls back to the Vike config defaults. useTheme()andThemeSettingsupdate the same client state and write it back tolocalStorage.- Theme tokens are expanded into
--color-*,--font-*,--space-*, and--radius.
This design is client-authoritative: defaults come from Vike config, and user changes are persisted on the client.
Quick start
The framework ships with two theme components:
ThemeToggle: switch betweenlight,dark, andsystemThemeSettings: a full panel for Primary, Neutral, Radius, Font, and export
<script setup>
import { ThemeToggle } from 'vike-vue-content/components/theme-toggle'
import { ThemeSettings } from 'vike-vue-content/components/theme-settings'
</script>
<template>
<header>
<h1>My App</h1>
<div>
<ThemeSettings />
<ThemeToggle />
</div>
</header>
</template>Remember to import:
import 'vike-vue-content/index.css'index.css is still required for component styles and theme-aware base styles. The actual CSS variable values are not hard-coded in CSS; they are applied at runtime by the first-paint init script and by useTheme().
If you use ThemeSettings, keep this boundary in mind:
theme,themes, andappearancein+config.tsdefine the site defaultsThemeSettingsonly overrides those defaults on the client- the override is stored in
localStorage['vvc-theme'] - clicking reset drops the override and goes back to the defaults from
+config.ts
Configure the default theme in Vike
import type { Config } from 'vike/types'
import vikeVue from 'vike-vue/config'
import vikeVueContent from 'vike-vue-content/config'
import { defineTheme } from 'vike-vue-content/theme'
const brand = defineTheme({
name: 'brand',
fonts: {
sans: "'Open Sans', sans-serif"
},
radius: '0.5rem',
light: {
primary: '#8b5cf6',
neutral: 'slate'
},
dark: {
primary: '#8b5cf6',
neutral: 'slate'
}
})
export default {
extends: [vikeVue, vikeVueContent],
theme: 'brand',
themes: [brand],
appearance: 'system'
} satisfies ConfigWhat each field means:
theme: the active theme namethemes: the registered theme objectsappearance: the default appearance, one of'light' | 'dark' | 'system'
If you omit everything, the built-in default is blue + slate + Inter + 0.25rem.
defineTheme()
defineTheme() accepts raw tokens and returns a normalized theme object.
import { defineTheme } from 'vike-vue-content/theme'
const brand = defineTheme({
name: 'brand',
fonts: {
sans: "'Outfit', sans-serif"
},
radius: '0.375rem',
spacing: {
container: '1.5rem'
},
light: {
primary: '#0f766e',
neutral: 'slate',
bg: '#fcfffe'
},
dark: {
primary: '#0f766e',
neutral: 'slate',
bg: '#081311'
}
})Normalization rules:
- fields under
light/darkbecome--color-* - fields under
fontsbecome--font-* - fields under
spacingbecome--space-* radiusbecomes--radius
There are two special tokens:
primary- accepts either a named preset or raw hex such as
'#0066cc' - raw hex is expanded with Chroma.js into
primary-lightandprimary-dark
- accepts either a named preset or raw hex such as
neutral- currently accepts named neutral presets only
- expands into semantic variables such as
muted,bg,surface,text, andborder
If light and dark share the same palette, you can also use colors as a shared fallback.
Built-in presets
Primary presets:
black red orange amber yellow lime green emerald teal cyan sky blue indigo violet purple fuchsia pink rose
Neutral presets:
slate gray zinc neutral stone
Radius presets:
0 0.125 0.25 0.375 0.5
Font presets:
Inter system-ui Roboto Open Sans Montserrat Poppins Outfit Raleway
Programmatic control
useTheme() reads the current default theme, applies it to the DOM, and persists user changes to localStorage.
<script setup lang="ts">
import { useTheme } from 'vike-vue-content/composables/theme'
const {
state,
theme,
isDark,
primary,
neutral,
radius,
font,
mode,
modes,
toggleDarkMode,
resetTheme,
exportCSS,
exportConfig,
exportVikeThemeConfig
} = useTheme()
</script>Typical usage:
primary.value = 'violet'
primary.value = '#0066cc'
neutral.value = 'zinc'
radius.value = 0.5
font.value = 'Outfit'
mode.value = 'dark'Notes:
modemaps toappearancetoggleDarkMode()cycles throughlight -> dark -> systemresetTheme()removes the user override and goes back to the Vike config defaultsexportVikeThemeConfig()returns normalized JSON that can be pasted back intothemes
If you only need persistence and not DOM application, use useThemeStorage().
CSS variable contract
The public surface of the theme system is a stable variable set:
| Variable | Meaning |
|---|---|
--color-primary | primary color |
--color-primary-light | lighter primary step |
--color-primary-dark | darker primary step |
--color-muted | neutral base |
--color-muted-light | lighter neutral step |
--color-muted-dark | darker neutral step |
--color-bg | page background |
--color-surface | surface background |
--color-surface-elevated | elevated / hover background |
--color-text | primary text |
--color-text-muted | secondary text |
--color-text-dimmed | dimmed text |
--color-border | border color |
--color-border-muted | softer border |
--font-sans | primary font |
--radius | radius |
--space-* | custom spacing tokens |
When dark mode is active, the root element receives a .dark class. In most cases you should consume variables directly instead of writing separate .dark overrides:
<style scoped>
.card {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
.card:hover {
background: var(--color-surface-elevated);
}
.link {
color: var(--color-primary);
}
</style>Export and low-level APIs
The ThemeSettings panel exposes two export buttons:
main.css: exports CSS for the current themevike-theme.json: exports a normalized theme object that can be pasted intothemes
If you want to handle theme compilation yourself, @vike-vue-content/theme also exposes low-level APIs:
import {
exportThemeCss,
exportVikeThemeConfig,
themeToVars,
themeToCss,
themeToAppearanceCss
} from 'vike-vue-content/theme'Typical use cases:
- generating static CSS files
- storing theme objects as JSON
- building a custom theme editor
- reusing the same token compiler outside Vue