vike-vue-content

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:

  1. Provide the default theme through theme, themes, and appearance in +config.ts.
  2. vike-vue-content/config automatically registers headHtmlBegin and injects a small init script before first paint.
  3. The init script reads localStorage['vvc-theme'] first; if there is no user override, it falls back to the Vike config defaults.
  4. useTheme() and ThemeSettings update the same client state and write it back to localStorage.
  5. 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 between light, dark, and system
  • ThemeSettings: 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, and appearance in +config.ts define the site defaults
  • ThemeSettings only 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 Config

What each field means:

  • theme: the active theme name
  • themes: the registered theme objects
  • appearance: 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 / dark become --color-*
  • fields under fonts become --font-*
  • fields under spacing become --space-*
  • radius becomes --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-light and primary-dark
  • neutral

    • currently accepts named neutral presets only
    • expands into semantic variables such as muted, bg, surface, text, and border

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:

  • mode maps to appearance
  • toggleDarkMode() cycles through light -> dark -> system
  • resetTheme() removes the user override and goes back to the Vike config defaults
  • exportVikeThemeConfig() returns normalized JSON that can be pasted back into themes

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:

VariableMeaning
--color-primaryprimary color
--color-primary-lightlighter primary step
--color-primary-darkdarker primary step
--color-mutedneutral base
--color-muted-lightlighter neutral step
--color-muted-darkdarker neutral step
--color-bgpage background
--color-surfacesurface background
--color-surface-elevatedelevated / hover background
--color-textprimary text
--color-text-mutedsecondary text
--color-text-dimmeddimmed text
--color-borderborder color
--color-border-mutedsofter border
--font-sansprimary font
--radiusradius
--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 theme
  • vike-theme.json: exports a normalized theme object that can be pasted into themes

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