Vue 3
Provider and composables for Vue 3, with scoped themes.
@themvora/vueVue 3v2.0.0StableSSR:NoZero-flash:Partial1Install
pnpm add @themvora/core @themvora/vue2Set up
<!-- No theme definition needed -->
<script setup lang="ts">
import { ThemeProvider } from "@themvora/vue";
</script>
<!-- No `themes` prop → built-in neutral theme.
default-theme picks the theme; initial-mode="system" is the opt-in for
following the OS. The provider hands that mode to BOTH the pre-paint
script it injects and the runtime, so the two cannot disagree. -->
<template>
<ThemeProvider default-theme="light" initial-mode="system">
<h1>Hello, themed world</h1>
</ThemeProvider>
</template>default-theme chooses the starting theme. Add initial-mode="system" to follow the visitor's OS on the first visit.3Add a small UI
Design system
ActiveTokens resolve at runtime, so this card follows whichever theme is active.
<template>
<div class="themvora-example-card">
<div class="themvora-example-header">
<h3 class="themvora-example-title">Design system</h3>
<span class="themvora-example-status">Active</span>
</div>
<p class="themvora-example-description">
Tokens resolve at runtime, so this card follows whichever theme is active.
</p>
<p class="themvora-example-meta">Updated 2 minutes ago</p>
<button class="themvora-example-button" type="button">View themes</button>
</div>
</template>4Style it with Themvora tokens
/* Themvora emits its design tokens as CSS custom properties on the root
element, so a stylesheet only has to read them. Nothing here is
theme-specific: these rules render every theme, including ones you add
later. */
html,
body {
background: var(--theme-color-background);
color: var(--theme-color-foreground);
}
.themvora-example-card {
display: grid;
gap: 10px;
max-width: 22rem;
padding: 16px;
border: 1px solid var(--theme-color-border);
border-radius: var(--theme-radius-lg, 12px);
background: var(--theme-color-card);
color: var(--theme-color-cardForeground);
font-family: inherit;
line-height: 1.5;
box-shadow: var(--theme-shadow-sm, 0 1px 2px rgb(0 0 0 / 0.06));
}
.themvora-example-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.themvora-example-title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.themvora-example-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--theme-color-success);
}
.themvora-example-status::before {
content: "";
width: 6px;
height: 6px;
border-radius: 9999px;
background: currentColor;
}
.themvora-example-description {
margin: 0;
font-size: 13px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-meta {
margin: 0;
font-size: 12px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-button {
justify-self: start;
padding: 8px 14px;
border: 0;
/* Presets and generated themes emit --theme-radius-lg and nothing else on
the radius scale, so the smaller step is derived from it rather than
assumed — the same proportion the utility-class mapping uses. */
border-radius: calc(var(--theme-radius-lg, 12px) * 0.8);
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.themvora-example-button:hover {
filter: brightness(1.08);
}
.themvora-example-button:focus-visible {
outline: 2px solid var(--theme-color-ring);
outline-offset: 2px;
}.themvora-example-card and its children read Themvora tokens, so switching the active theme repaints them — no re-render, and no per-theme CSS.The `body` rule uses exactly the two foundational colours — `background` and `foreground`. Every component rule reads one of the 21 colours `validateTheme()` requires, so any valid theme emits it. Radius and shadow are optional and carry fallbacks; no typography token is read, so a theme without a typography group still renders correctly.
5Run and choose a theme
<script setup lang="ts">
import { useTheme } from "@themvora/vue";
const { theme, mode, toggleTheme } = useTheme();
</script>
<template>
<main class="page">
<h1>Themvora</h1>
<p>Current theme: {{ theme.name }} ({{ mode }})</p>
<button class="primary" @click="toggleTheme">Toggle theme</button>
<section class="card">
<h2>Themed card</h2>
<p class="muted">Surface, border and text follow the theme.</p>
</section>
</main>
</template>
<style scoped>
.page {
background: var(--theme-color-background);
color: var(--theme-color-foreground);
min-height: 100vh;
padding: 2rem;
}
.primary {
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
border: 0;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
}
.card {
background: var(--theme-color-card);
border: 1px solid var(--theme-color-border);
border-radius: 0.75rem;
margin-top: 1.5rem;
padding: 1rem;
}
.muted {
color: var(--theme-color-mutedForeground);
}
</style>Theme configuration
<script setup lang="ts">
import { ThemeProvider } from "@themvora/vue";
</script>
<template>
<!-- `default-theme` picks the theme. Add `initial-mode="system"` to follow
the OS preference instead of the theme's own mode. -->
<ThemeProvider default-theme="mint-light">
<h1>Hello, themed world</h1>
</ThemeProvider>
</template>theme.config.ts instead and let themvoraVitePlugin() derive the pre-paint bootstrap from the same file — the provider then takes no theme props. The plugin is optional either way.What's Available
@themvora/vue ships 17 exports in 5 categories. Click an export to see what it does and jump to the code that uses it.
Three pages cover this package, and they do different jobs: this index says what exists, the examples page shows each export in code with the problem it solves, and the API reference gives exact signatures and return types.
Use Cases
Short snippets you can drop into an existing app. For the same problems worked through end to end — problem, APIs, expectations, limitations — see the Vue 3 examples.
Toggle light / dark
useTheme exposes refs and setters — reactive in templates and scripts.
<script setup lang="ts">
import { useTheme } from "@themvora/vue";
const { theme, mode, family, setMode, setFamily, toggleTheme } = useTheme();
</script>
<template>
<button @click="toggleTheme">{{ theme.name }} · {{ mode }}</button>
<button @click="setMode('dark')">Dark</button>
<select :value="family" @change="(e: Event) => setFamily((e.target as HTMLSelectElement).value)">
<option value="default">Default</option>
<option value="mint">Mint</option>
</select>
</template>Undo / redo theme changes
History state is reactive; use it to build undo/redo controls.
<script setup lang="ts">
import { useThemeHistory, useThemeRuntime } from "@themvora/vue";
const { undo, redo, canUndo, canRedo } = useThemeHistory();
const runtime = useThemeRuntime();
</script>
<template>
<button :disabled="!canUndo" @click="undo">Undo</button>
<button :disabled="!canRedo" @click="redo">Redo</button>
<button @click="runtime.update({ colors: { primary: '#6366f1' } })">
Make primary indigo
</button>
</template>Scope a subtree
ThemeScope applies a theme to a slot with scoped CSS variables.
<script setup lang="ts">
import { ThemeScope } from "@themvora/vue";
import type { ThemeTransitionOptions } from "@themvora/core";
const transition: ThemeTransitionOptions = { duration: 300, easing: "ease" };
</script>
<template>
<Sidebar />
<ThemeScope theme="plum-dark" :transition="transition">
<DataViz />
</ThemeScope>
</template>Lifecycle events
Subscribe to typed lifecycle events and react to changes.
<script setup lang="ts">
import { onMounted } from "vue";
import { useThemeLifecycle, useThemePacks } from "@themvora/vue";
import { getAccessibilityProfiles } from "@themvora/core";
const { on } = useThemeLifecycle();
const applyPack = useThemePacks();
onMounted(() => {
on("beforeThemeChange", (e) => console.log("theme changing to", e.next.name));
});
</script>
<template>
<button @click="applyPack({ name: 'a11y', themes: getAccessibilityProfiles() })">
Apply High Contrast pack
</button>
</template>Smooth theme transitions
Enable CSS transitions on theme changes for a polished user experience.
<script setup lang="ts">
import { ThemeProvider } from "@themvora/vue";
</script>
<template>
<ThemeProvider
:transition="{ enabled: true, duration: 300, easing: 'ease-in-out' }"
>
<YourView />
</ThemeProvider>
</template>A scoped panel with history controls
ThemeScope is a component you place around a subtree. It reads the runtime from context, so the same composables that drive the page drive the scope.
<script setup lang="ts">
// Import both explicitly — `ThemeScope` is only globally registered if your
// entry does `app.use(ThemeScope)`.
import { ThemeScope, useThemeHistory } from "@themvora/vue";
const { undo, redo, canUndo, canRedo } = useThemeHistory();
</script>
<template>
<div>
<button @click="undo" :disabled="!canUndo">Undo</button>
<button @click="redo" :disabled="!canRedo">Redo</button>
<ThemeScope theme="plum-dark">
<p>This subtree is always plum-dark.</p>
</ThemeScope>
</div>
</template>A plum-dark panel with undo and redo. canUndo and canRedo are refs, so the buttons disable themselves as the history moves.
The complete runnable app behind this guide is examples/apps/vue — listed with the other example apps.