close
Skip to content

pooyagolchian/vue-multiple-themes

Repository files navigation

vue-multiple-themes

vue-multiple-themes

Dynamic multi-theme support for Vue 3 — CSS custom properties, TailwindCSS (with full opacity modifier support), WCAG contrast utilities, white-label brand contexts, and a reactive composable API.

npm version npm downloads License: MIT TypeScript Vue 3 TailwindCSS Zero Dependencies Tests


Why vue-multiple-themes?

Most Vue theming solutions only handle light/dark toggling. vue-multiple-themes is a complete multi-theme engine for production applications.

Comparison with Alternatives

Feature vue-multiple-themes @vueuse useColorMode nuxt-color-mode
Multiple themes (3+) ✅ Unlimited ⚠️ Manual ⚠️ Light/Dark only
TailwindCSS v3 & v4 plugin ✅ Built-in
Opacity modifiers (bg-primary/50)
WCAG contrast utilities ✅ 5+ functions
Theme generation from 1 color
White-label / namespace createBrandContext()
Color utility library ✅ 20+ functions
Preset themes ✅ 7 included
System preference
TypeScript ✅ Full
Zero dependencies

📖 Full documentation →


Development

Prerequisites: pnpm v9+ and Node.js v18+

# Install all workspace dependencies
pnpm install

# Run the interactive playground (Vite dev server → http://localhost:5173)
pnpm dev

# Build the library
pnpm build

# Build the playground for production (deployed to GitHub Pages)
pnpm build:playground

Features

  • Vue 3 Optimized — leverage the latest Composition API and <script setup>
  • TypeScript — full type definitions included
  • CSS custom properties — semantic --vmt-* variables injected automatically
  • TailwindCSS pluginbg-vmt-primary, text-vmt-foreground, etc., with full opacity modifier support (bg-vmt-primary/50)
  • Tailwind v3 & v4 — dedicated plugin for each major version
  • 7 preset themes — light, dark, sepia, ocean, forest, sunset, winter
  • Dynamic theme generation — create themes from a single brand color
  • Color utilities — lighten, darken, mix, contrast ratio, WCAG compliance
  • useTheme() composable — reactive, SSR-safe, localStorage-persistent, luminance-based isDark
  • System preference detection — auto-select light/dark based on OS setting
  • Bring-your-own iconsVmtIcon :as="SunIcon" forwards to any Vue icon component (lucide-vue-next, @heroicons/vue, …); no icon data shipped in the bundle
  • White-label / multi-tenant readycreateBrandContext() creates fully isolated namespaced theme engines
  • Headless componentsVmtThemePicker with keyboard nav & ARIA, VmtIcon
  • Zero runtime dependencies (only vue peer dependency)

Installation

# pnpm (recommended)
pnpm add vue-multiple-themes

# npm
npm install vue-multiple-themes

# yarn
yarn add vue-multiple-themes

Quick Start

Vue 3 — Composition API

// 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', // 'attribute' | 'class' | 'both'
  storage: 'localStorage', // 'localStorage' | 'sessionStorage' | 'none'
});
app.mount('#app');
<!-- App.vue -->
<script setup lang="ts">
import { useTheme, PRESET_THEMES } from 'vue-multiple-themes';

const ts = useTheme({ themes: PRESET_THEMES });
// ts.current, ts.isDark, ts.theme are reactive (auto-unwrapped)
</script>

<template>
  <button v-for="t in ts.themes" :key="t.name" @click="ts.setTheme(t.name)">
    {{ t.label }} (Active: {{ ts.current === t.name }})
  </button>
  <p>Dark mode: {{ ts.isDark }}</p>
</template>

Icons

VmtIcon is a thin forwarder — it accepts any Vue icon component via the as prop and forwards size, color, and strokeWidth. No SVG data lives in the library.

# Install any Vue icon library — e.g.
pnpm add lucide-vue-next
import { Sun, Moon, Palette } from 'lucide-vue-next'
import { VmtIcon } from 'vue-multiple-themes'
<!-- Render any icon through VmtIcon -->
<VmtIcon :as="Sun"  :size="24" color="currentColor" />
<VmtIcon :as="Moon" :size="20" :stroke-width="1.5" />

<!-- Or use the component directly — same props accepted -->
<Sun :size="24" color="currentColor" />

Assign icons to theme definitions:

import { Sun, Moon } from 'lucide-vue-next'

const themes = [
  { name: 'light', label: 'Light', icon: Sun,  colors: { ... } },
  { name: 'dark',  label: 'Dark',  icon: Moon, colors: { ... } },
]

Attach icons to the ready-made PRESET_THEMES:

import { Sun, Moon, Monitor, Coffee, Leaf, Droplets, Flame, Snowflake } from 'lucide-vue-next'
import { PRESET_THEMES } from 'vue-multiple-themes'
import type { Component } from 'vue'

const presetIcons: Record<string, Component> = {
  light: Sun, dark: Moon, system: Monitor,
  cafe: Coffee, nature: Leaf, ocean: Droplets,
  flame: Flame, nord: Snowflake,
}

const themes = PRESET_THEMES.map(t => ({ ...t, icon: presetIcons[t.name] }))

CSS Custom Properties

Themes inject dual CSS variables on the target element (default: <html>):

/* Channel format (for Tailwind opacity modifiers) */
--vmt-primary: 59 130 246;

/* Full color (for direct CSS use) */
--vmt-primary-color: rgb(59 130 246);

Use the -color suffixed variables in custom CSS:

.card {
  background: var(--vmt-background-color);
  color: var(--vmt-foreground-color);
  border: 1px solid var(--vmt-border-color);
}

/* Manual opacity using the channel format */
.overlay {
  background: rgb(var(--vmt-primary) / 0.5);
}

TailwindCSS Integration

Tailwind v3

// tailwind.config.js
import { createVmtPlugin } from 'vue-multiple-themes/tailwind';
import { PRESET_THEMES } from 'vue-multiple-themes';

export default {
  content: ['./src/**/*.{vue,ts,tsx}'],
  plugins: [
    createVmtPlugin({
      themes: PRESET_THEMES,
      strategy: 'both',
      darkThemes: ['dark'], // enables Tailwind `dark:` modifier
    }),
  ],
};

Opacity modifiers work out of the box:

<div class="bg-vmt-primary/50 text-vmt-text border-vmt-border/75">
  <span class="text-vmt-primary/80">Semi-transparent text</span>
</div>

All Tailwind utilities are available: bg-, text-, border-, ring-, divide-, placeholder-, outline-, shadow-, accent-, caret-, fill-, stroke-, gradients (from-, via-, to-), and more.

Tailwind v4

import { generateVmtCssForV4 } from 'vue-multiple-themes/tailwind-v4';
import { PRESET_THEMES } from 'vue-multiple-themes';

// Outputs @theme and @custom-variant blocks for your CSS
const css = generateVmtCssForV4({
  themes: PRESET_THEMES,
  strategy: 'both',
  darkThemes: ['dark'],
});

Preset Themes

Name Description
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
import { PRESET_THEMES, oceanTheme, forestTheme } from 'vue-multiple-themes';

Dynamic Theme Generation

Create light/dark theme pairs from a single brand color:

import { generateThemePair } from 'vue-multiple-themes';

const [light, dark] = generateThemePair('#6366f1'); // indigo

Generate a full color scale:

import { generateColorScale } from 'vue-multiple-themes';

const scale = generateColorScale('#6366f1', 9); // 9-step palette

Color Utilities

All utilities are SSR-safe (no DOM dependency) and tree-shakeable:

import {
  lighten,
  darken,
  mix,
  contrastRatio,
  autoContrast,
  checkContrast,
  complementary,
  triadic,
  analogous,
  normalizeToRgbChannels,
} from 'vue-multiple-themes';

lighten('#6366f1', 0.2);       // lighter hex
darken('#6366f1', 0.3);        // darker hex
mix('#ff0000', '#0000ff', 0.5); // purple blend
contrastRatio('#000', '#fff');  // 21
autoContrast('#6366f1');        // '#ffffff' or '#000000'
checkContrast('#6366f1', '#fff'); // { ratio, aa, aaa, aaLarge, aaaLarge }
normalizeToRgbChannels('#6366f1'); // '99 102 241'

API

useTheme(options)

Option Type Default Description
themes ThemeDefinition[] Available themes (required)
defaultTheme string first theme Initial theme name
strategy 'attribute' | 'class' | 'both' 'attribute' How theme is applied to DOM
target string 'html' Target element selector
storage 'localStorage' | 'sessionStorage' | 'none' 'localStorage' Where to persist the active theme
storageKey string 'vmt-theme' Storage key for persistence
namespace string Isolate state for white-label / multi-tenant setups
respectSystemPreference boolean false Auto-select theme matching OS mode
onThemeChange (newTheme, oldTheme) => void Callback on every theme change

Returns:

Property Type Description
current readonly string Active theme name (reactive)
theme readonly ThemeDefinition Active theme definition (reactive)
isDark readonly boolean Luminance-based dark detection (reactive)
themes readonly ThemeDefinition[] All available themes
resolvedColors readonly Record<string, {r,g,b}> Active theme colors as RGB objects
setTheme() (name: string) => void Activate a theme by name
nextTheme() () => void Advance to the next theme
prevTheme() () => void Go back to the previous theme
toggleTheme() () => void Toggle between first two themes

Note: current, theme, isDark, and resolvedColors are reactive properties wrapped in reactive(). Use them directly in templates: {{ ts.current }}. To watch() them, use a getter: watch(() => ts.current, ...).

createBrandContext(options)

Option Type Description
namespace string (required) Unique brand identifier — scopes style tag, singleton, and provide key
+ all useTheme options All ThemeOptions fields accepted as context defaults

Returns: { namespace, useTheme(overrides?), BrandPlugin }


White-label & Multi-tenant

Use createBrandContext() when you need multiple independent theme engines in the same Vue app — e.g. white-label products, micro-frontends, or embeddable widgets.

Each context isolates:

  • Its injected <style> tag (id="vmt-theme-styles-<namespace>")
  • Its singleton reactive state (keyed by <namespace>:<storageKey>)
  • Its Vue provide key ("vmt:options:<namespace>")
  • Its Tailwind color namespace (bg-acme-primary vs bg-beta-primary)
import { createBrandContext } from 'vue-multiple-themes'

export const acme = createBrandContext({
  namespace:    'acme',
  storageKey:   'acme-theme',
  cssVarPrefix: '--acme-',
  themes:       acmeThemes,
  defaultTheme: 'acme-light',
  strategy:     'attribute',
})

export const beta = createBrandContext({
  namespace:    'beta',
  storageKey:   'beta-theme',
  cssVarPrefix: '--beta-',
  themes:       betaThemes,
  defaultTheme: 'beta-light',
  strategy:     'attribute',
})

// main.ts — install both plugins
app.use(acme.BrandPlugin)
app.use(beta.BrandPlugin)
<!-- Component.vue -->
<script setup lang="ts">
import { acme, beta } from './brands'

const acmeState = acme.useTheme()
const betaState = beta.useTheme()
</script>

<template>
  <button @click="acmeState.toggleTheme()">Acme: {{ acmeState.current }}</button>
  <button @click="betaState.toggleTheme()">Beta: {{ betaState.current }}</button>
</template>

Migrating from v5 to v6

Breaking Changes

  1. CSS variable format changed--vmt-primary now contains RGB channels (59 130 246) instead of hex. Use --vmt-primary-color for the full rgb() value in custom CSS.

  2. Tailwind plugin API changedcreateVmtPlugin() now requires a themes option:

    // Before (v5)
    createVmtPlugin()
    // After (v6)
    createVmtPlugin({ themes: PRESET_THEMES })
  3. persist option renamed — Use storage: 'localStorage' instead of persist: true.

  4. useTheme() return type changed — Returns a reactive() object. Properties are accessed directly (no .value needed). currentTheme/currentName renamed to theme/current.

  5. isDark uses luminance — Now calculated from background color luminance instead of name matching.

  6. Icon API changedThemeDefinition.icon is now Component (not string). VmtIcon uses :as instead of :name. No icon data is bundled — bring your own library:

    // Before (v5 — string name, non-functional)
    { name: 'light', icon: 'sun', colors: { ... } }
    // <VmtIcon name="sun" :size="24" />
    
    // After (v6 — real Vue component)
    import { Sun } from 'lucide-vue-next'
    { name: 'light', icon: Sun, colors: { ... } }
    // <VmtIcon :as="Sun" :size="24" />

Documentation

Full documentation and live demos:

https://pooyagolchian.github.io/vue-multiple-themes/

For AI / LLMs

This project includes an llms.txt file following the llmstxt.org specification, providing structured API documentation for AI assistants.


License

MIT © Pooya Golchian