I have been building UIs with Vue for years and one pattern comes up constantly — you need more than dark/light. Clients want seasonal themes, brand-specific palettes, and accessibility-compliant contrasts. I extracted all of that into a standalone, typed library: vue-multiple-themes.
What It Solves
The standard approach is toggling a .dark class on <html> and writing a wall of CSS overrides. That works for two themes. Scale to three or more and you get duplicated selectors, fragile specificity battles, and no tooling for generating accessible palettes.
vue-multiple-themes replaces that with:
- CSS custom properties (
--vmt-*) injected at the target element — every theme is a swap of values at one cascade layer - A reactive
useTheme()composable accessible anywhere in the component tree - 7 preset themes ready to use immediately
- A TailwindCSS plugin that exposes those tokens as Tailwind utilities
- WCAG color utilities for contrast checking, mixing, and palette generation — all SSR-safe
Installation
pnpm add vue-multiple-themesRequires Vue 2.7+ or Vue 3. Zero runtime dependencies beyond Vue itself.
Quick Start — Vue 3
Register the plugin once in main.ts:
import { createApp } from 'vue';
import { VueMultipleThemesPlugin } from 'vue-multiple-themes';
import App from './App.vue';
const app = createApp(App);
app.use(VueMultipleThemesPlugin, {
defaultTheme: 'dark',
strategy: 'attribute',
persist: true,
});
app.mount('#app');Then use useTheme() anywhere:
<script setup lang="ts">
import { useTheme, PRESET_THEMES } from 'vue-multiple-themes';
const { currentTheme, setTheme, themes } = useTheme({ themes: PRESET_THEMES });
</script>
<template>
<button v-for="t in themes" :key="t.name" @click="setTheme(t.name)">
{{ t.label }}
</button>
</template>CSS Custom Properties
Once a theme is active, --vmt-* variables are available on <html>. Style components against them:
.card {
background: var(--vmt-background);
color: var(--vmt-foreground);
border: 1px solid var(--vmt-border);
}Switching themes updates every component instantly — no re-renders required.
The 7 Preset Themes
| Name | Character |
|---|---|
light | Clean white + indigo |
dark | Dark gray + violet |
sepia | Warm parchment browns |
ocean | Deep sea blues |
forest | Rich greens |
sunset | Warm oranges & reds |
winter | Icy blues & whites |
Dynamic Theme Generation
import { generateThemePair, generateColorScale } from 'vue-multiple-themes';
const { light, dark } = generateThemePair('#6366f1');
const scale = generateColorScale('#6366f1', 9);Ideal for SaaS products where each tenant sets a brand color and the full UI adapts automatically.
TailwindCSS Integration
const { createVmtPlugin } = require('vue-multiple-themes/tailwind');
module.exports = { plugins: [createVmtPlugin()] };<div class="bg-vmt-surface text-vmt-foreground border-vmt-border">
Themes itself automatically on switch
</div>WCAG Utilities
Pure functions — no DOM, fully SSR-safe, tree-shakeable:
import { contrastRatio, autoContrast, checkContrast, lighten, darken } from 'vue-multiple-themes';
contrastRatio('#6366f1', '#ffffff'); // 4.54
autoContrast('#6366f1'); // '#ffffff'
checkContrast('#6366f1', '#ffffff');
// { ratio: 4.54, aa: true, aaa: false, aaLarge: true, aaaLarge: true }useTheme() API
| Option | Type | Default | Description |
|---|---|---|---|
themes | ThemeDefinition[] | preset list | Available themes |
defaultTheme | string | light | Initial theme |
strategy | attribute / class / both | attribute | DOM application strategy |
persist | boolean | true | Save to localStorage |
storageKey | string | vmt-theme | localStorage key |
Returns: { currentTheme, currentName, themes, setTheme, nextTheme, prevTheme }
Vue 2 Support
import Vue from 'vue';
import { VueMultipleThemesPlugin } from 'vue-multiple-themes';
Vue.use(VueMultipleThemesPlugin, { defaultTheme: 'light' });