Vue 3 examples
Each export in @themvora/vue, with the problem it solves and the code that solves it.
@themvora/vueVue 3v2.0.0StableThe Vue integration is built on Composition API reactivity: ThemeProvider installs the runtime, useTheme() hands back refs, and every composable must be called in setup so its subscriptions are tied to the component instance.
Because the composables subscribe in onMounted and unsubscribe in onUnmounted, they are safe during SSR — the server render uses the initial values and the live ones arrive after hydration.
1The provider, then the composables
app.use() or by wrapping the root component. Both do the same thing; app.use() is the idiomatic choice for an app-wide provider.import { createApp } from "vue";
import { ThemeProvider } from "@themvora/vue";
import App from "./App.vue";
createApp(App)
.use(ThemeProvider, { initialMode: "system" })
.mount("#app");<script setup lang="ts">
import { useTheme } from "@themvora/vue";
const { theme, effectiveMode, toggleTheme } = useTheme();
</script>
<template>
<main>
<h1>{{ theme.name }}</h1>
<p>Currently rendering in {{ effectiveMode }} mode.</p>
<button @click="toggleTheme">Switch mode</button>
</main>
</template>ThemeProvider accepts every runtime option plus initialMode and initialFamily, so the built-in themes need no configuration.
2Pickers, previews, and no flash on SSR
A family picker driven by the registry
A hand-kept array of family names in the settings page drifts from what the app actually ships — a renamed family lingers, and a pack's families never appear.
useProvidedThemes()useTheme()<script setup lang="ts">
import { useProvidedThemes, useTheme } from "@themvora/vue";
const families = useProvidedThemes();
const { family, setFamily } = useTheme();
function handleFamilyChange(event: Event) {
const target = event.currentTarget;
if (!(target instanceof HTMLSelectElement)) return;
setFamily(target.value);
}
</script>
<template>
<label>
Theme family
<select :value="family" @change="handleFamilyChange">
<option v-for="entry in families" :key="entry.name" :value="entry.name">
{{ entry.name }}
</option>
</select>
</label>
</template>useProvidedThemes() returns a ref, so the template unwraps it and a computed over it re-runs when the registry changes — including when a pack is installed after mount. The options come from the ref and the active one matches useTheme(). Choosing another repaints immediately; a pack installed later adds its families to the list.
A mode toggle that reports the truth
The toggle's icon says light while the page is dark, because the selection is system.
useEffectiveThemeMode()useTheme()<script setup lang="ts">
import { computed } from "vue";
import { useEffectiveThemeMode, useTheme } from "@themvora/vue";
const effectiveMode = useEffectiveThemeMode();
const { mode, setMode } = useTheme();
const next = computed(() => (effectiveMode.value === "dark" ? "light" : "dark"));
</script>
<template>
<button
:aria-pressed="effectiveMode === 'dark'"
:aria-label="`Switch to ${next} mode`"
@click="setMode(next)"
>
{{ effectiveMode === "dark" ? "🌙 Dark" : "☀️ Light" }}
<span v-if="mode === 'system'"> (auto)</span>
</button>
</template>effectiveMode is the applied appearance; mode is the standing request. The label needs the first, the click needs the second. Icon and label always match the page; a click pins an explicit mode so a system visitor stops following the OS.
Preview families without switching to them
A gallery should show each family before the visitor commits, without repainting the whole page per preview.
ThemeScopeuseProvidedThemes()<script setup lang="ts">
import { ThemeScope, useProvidedThemes, useTheme } from "@themvora/vue";
const families = useProvidedThemes();
const { setFamily } = useTheme();
</script>
<template>
<div class="grid grid-cols-2 gap-4">
<button
v-for="entry in families"
:key="entry.name"
@click="setFamily(entry.name)"
>
<ThemeScope :family="entry.name" class="p-4 rounded-lg">
<strong>{{ entry.name }}</strong>
<p class="muted">The quick brown fox</p>
</ThemeScope>
</button>
</div>
</template>ThemeScope writes the scoped variables onto its own wrapper and renders its slot inside them, so any number of previews coexist and the global selection never moves. Each card renders in its own family; clicking one commits the choice through the setter.
Zero flash on a server-rendered Vue app
A Nuxt-less SSR setup paints the default theme and corrects it once the client bundle runs.
createVueThemeBootstrapScript()ThemeProviderimport { createVueThemeBootstrapScript } from "@themvora/vue";
import { getBuiltInThemes } from "@themvora/core";
const themes = getBuiltInThemes();
// Emit inside <head>, before any stylesheet that depends on the variables.
export const themeScript = createVueThemeBootstrapScript({
themes,
defaultTheme: "mint-light",
});The generated script applies the persisted selection in <head>, before the body is parsed, so there is nothing to correct. It is emitted with Vue's storage key and prefix, which is what makes it agree with the provider's persistence. The first paint is the persisted theme; no correction frame on reload.
Watch out: The script and the provider must share the same theme registry. Emit the script from the same module that defines the themes.
3Composables, components, and bindings
Setup
Two ways to establish the runtime: the provider component, or an explicit provide/inject pair when the runtime has to be created elsewhere.
ThemeProvider
A provider component that creates the runtime, installs the DOM and CSS-variable bindings, and exposes it through Vue's provide/inject. It accepts every runtime option and registers itself with app.use().
Once, at the root — app.use(ThemeProvider, options) or a <ThemeProvider> around the root component.
<script setup lang="ts">
import { ThemeProvider } from "@themvora/vue";
import { getBuiltInThemes } from "@themvora/core";
const themes = getBuiltInThemes();
</script>
<template>
<ThemeProvider
:themes="themes"
default-theme="mint-light"
initial-mode="system"
:transition="{ enabled: true, duration: 250 }"
>
<RouterView />
</ThemeProvider>
</template>The active theme's variables and data-theme are applied to the document, and every composable below the provider resolves the same runtime.
provideThemeRuntime() / useThemeRuntime()
The explicit provide/inject pair. provideThemeRuntime(runtime) publishes a runtime you created; useThemeRuntime() retrieves it and throws when none was provided.
The runtime already exists outside the component tree — created in a plugin, shared with a non-Vue widget, or built from a config the provider cannot express.
import type { App } from "vue";
import { createThemeRuntime, getBuiltInThemes } from "@themvora/core";
export const runtime = createThemeRuntime({
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
});
export function installTheme(app: App) {
app.provide(Symbol.for("themvora-runtime"), runtime);
}<script setup lang="ts">
import { useThemeRuntime } from "@themvora/vue";
const runtime = useThemeRuntime();
// e.g. a custom adapter, or a subscription the composables do not expose.
</script>Every composable resolves the provided runtime. useThemeRuntime() gives direct access for anything the composables do not cover.
Relatedvue-providervue-lifecycle
Composables
Reads are refs, so a template unwraps them automatically and script code uses .value. Actions are plain functions that write to the runtime.
useTheme()useEffectiveThemeMode()useThemeHistory()useThemeBatch()useThemeSnapshot() / useThemeRestore()useThemeLifecycle()useThemePacks()useThemeSchedule()useTheme()
Returns theme, mode, effectiveMode and family as Refs, plus setMode, setFamily and toggleTheme as plain functions.
A component needs more than one slice — a settings panel, or a header showing the theme name and a toggle.
<script setup lang="ts">
import { useTheme } from "@themvora/vue";
const { theme, mode, effectiveMode, family, setMode, toggleTheme } = useTheme();
</script>
<template>
<section>
<h2>{{ theme.name }}</h2>
<p>Family {{ family }} · requested {{ mode }} · showing {{ effectiveMode }}</p>
<button @click="toggleTheme">Toggle</button>
<button @click="setMode('system')">Follow system</button>
</section>
</template>The component re-renders when any of the four refs changes; the setters update the runtime and the document immediately.
useEffectiveThemeMode()
A Ref<'light' | 'dark'> holding the mode actually applied — never system.
Any label, icon or aria-pressed that states the current appearance. Binding those to mode is the classic bug: with system it never moves.
<script setup lang="ts">
import { useEffectiveThemeMode } from "@themvora/vue";
const effectiveMode = useEffectiveThemeMode();
</script>
<template>
<span :aria-label="`${effectiveMode} mode`">
{{ effectiveMode === "dark" ? "🌙" : "☀️" }}
</span>
</template>Updates on a click, an OS appearance change, and a sunrise/sunset transition. During SSR it reports what the server rendered.
Relatedvue-use-themevue-schedule
useThemeHistory()
Reactive canUndo, canRedo and history refs plus undo, redo, clear and jump actions.
A theme editor or live preview where the visitor experiments and wants to step back or jump to a point.
<script setup lang="ts">
import { useThemeHistory } from "@themvora/vue";
const { undo, redo, canUndo, canRedo, clear, jump, history } = useThemeHistory();
</script>
<template>
<div>
<button :disabled="!canUndo" @click="undo">Undo</button>
<button :disabled="!canRedo" @click="redo">Redo</button>
<button :disabled="!canUndo && !canRedo" @click="clear">Reset</button>
<ol>
<li v-for="(entry, index) in history" :key="index">
<button @click="jump(index)">{{ entry.theme.name }}</button>
</li>
</ol>
</div>
</template>The refs update as history changes, so buttons disable themselves; jump(index) moves straight to an entry.
Relatedvue-snapshotvue-batch
useThemeBatch()
Returns a function that runs its callback with DOM writes and notifications coalesced into one flush — and, in Vue's implementation, one history entry.
Several changes belong together: setting family and mode at once, or applying a token patch plus a selection.
<script setup lang="ts">
import { useThemeBatch, useThemeRuntime } from "@themvora/vue";
const batch = useThemeBatch();
const runtime = useThemeRuntime();
function applyPreset() {
batch(() => {
runtime.selection.setFamily("plum");
runtime.selection.setMode("dark");
});
}
</script>
<template>
<button @click="applyPreset">Plum, dark</button>
</template>One re-render and one transition for the group; no intermediate state is painted.
Relatedvue-historyvue-provider
useThemeSnapshot() / useThemeRestore()
Two functions: one captures the full runtime state as a serialisable value, the other re-applies a captured state.
Named presets the visitor saves, or a checkpoint around a destructive edit. History is step-by-step; snapshots are named states.
<script setup lang="ts">
import { shallowRef } from "vue";
import { useThemeRestore, useThemeSnapshot } from "@themvora/vue";
const snapshot = useThemeSnapshot();
const restore = useThemeRestore();
const saved = shallowRef<ReturnType<typeof snapshot> | null>(null);
</script>
<template>
<div>
<button @click="saved = snapshot()">Save this look</button>
<button :disabled="!saved" @click="saved && restore(saved)">Restore</button>
</div>
</template>Restoring replaces the active theme and selection and notifies subscribers once.
Relatedvue-history
useThemeLifecycle()
A typed subscription to runtime lifecycle events such as beforeThemeChange and afterPersist.
You need to react to a change rather than to state — analytics, syncing an external store, or a value that must not trigger another render.
<script setup lang="ts">
import { onUnmounted } from "vue";
import { useThemeLifecycle } from "@themvora/vue";
const { on } = useThemeLifecycle();
// The composable's own subscriptions are cleaned up with the component; this
// one is ours to release.
const off = on("afterThemeChange", ({ theme }) => {
window.dispatchEvent(new CustomEvent("app:theme-changed", { detail: theme.name }));
});
onUnmounted(off);
</script>
<template><span /></template>The handler runs when the event fires and the returned unsubscribe stops it. Subscribing does not cause re-renders.
Relatedvue-provide-runtimevue-batch
useThemePacks()
Returns a function that installs a theme pack into the runtime's registry at runtime.
Themes arrive after mount: fetched from an API, generated from a brand colour, or code-split out of the initial bundle.
<script setup lang="ts">
import { watchEffect } from "vue";
import { useThemePacks } from "@themvora/vue";
import { generateTheme } from "@themvora/core";
const props = defineProps<{ seed: string }>();
const usePack = useThemePacks();
watchEffect(() => {
const generated = generateTheme({ seed: props.seed, family: "brand" });
usePack({ themes: [generated.light, generated.dark] });
});
</script>
<template><span /></template>The registry grows and consumers of useProvidedThemes() see the new families. The active selection is untouched unless the pack changes it.
useThemeSchedule()
Returns a controller: a state ref (enabled, active, status, sunrise, sunset, nextTransition) plus enable(), disable() and set(). The underlying schedule is null when the provider has no scheduled option, and the methods become no-ops.
A settings row that turns daylight following on or off, or a readout of the next automatic switch.
<script setup lang="ts">
import { useThemeSchedule } from "@themvora/vue";
const schedule = useThemeSchedule();
</script>
<template>
<label v-if="schedule.schedule">
<input
type="checkbox"
:checked="schedule.state.enabled"
@change="schedule.state.enabled ? schedule.disable() : schedule.enable()"
/>
Follow daylight
<small v-if="schedule.state.nextTransition">
Next switch: {{ schedule.state.nextTransition.toLocaleTimeString() }}
</small>
</label>
</template>state fills in after mount — deliberately, so the server render is deterministic — and updates as the sun moves.
Library adapters
The framework package does not re-export the adapters — install the adapter package alongside @themvora/vue. What matters here is timing: these composables register on mount, so nothing they write reaches server-rendered markup.
Adapter framework subpaths
Four adapter packages (@themvora/shadcn, -bootstrap, -daisyui, -open-props) publish a /vue entry next to their other framework subpaths. The subpath exports the composable; the package root exports the factory and its types.
Any time the adapter is installed from a .vue file. Importing the root instead gets you a factory with no lifecycle — nothing registers it and nothing disposes it.
// Vue wrapper (composable) — use this in a component.
import { useShadcnTheme } from "@themvora/shadcn/vue";
// Framework-neutral factory — no lifecycle wiring.
import { createShadcnAdapter } from "@themvora/shadcn/factory";
// The adapter's own stylesheet, imported once.
import "@themvora/shadcn/shadcn.css";The adapter is tied to the component that called the composable: its variables are removed when that component unmounts.
useShadcnTheme(runtime) / useBootstrapTheme(runtime)
A composable that installs the adapter's CSS-variable factory on the runtime you pass. It registers in onMounted and disposes in onUnmounted, so nothing the adapter writes exists during server render: the first paint carries Themvora's own tokens, and the library's variables appear once the component mounts.
A Vue app on shadcn/ui or Bootstrap where the library should read Themvora's palette rather than a second hand-written one. If those variables have to be in the SSR markup, install the factory on the server instead — the entry below shows how.
<script setup lang="ts">
import "@themvora/shadcn/shadcn.css";
import { useThemeRuntime } from "@themvora/vue";
import { useShadcnTheme } from "@themvora/shadcn/vue";
// Installs the binding on mount, disposes it on unmount.
useShadcnTheme(useThemeRuntime());
</script>
<template><slot /></template>The library's variables track the active theme from mount onward, and the host component's unmount removes them.
useDaisyTheme(runtime) / useOpenPropsTheme(runtime)
daisyUI and Open Props take the same route: a composable on the adapter's /vue subpath with the same onMounted registration. Both take the runtime as an argument rather than looking it up, so pass the one from useThemeRuntime().
The app styles with daisyUI's semantic classes (btn, card, input) or Open Props' custom properties and should follow Themvora's selection.
<script setup lang="ts">
import "@themvora/daisyui/daisyui.css";
import { useThemeRuntime } from "@themvora/vue";
import { useDaisyTheme } from "@themvora/daisyui/vue";
useDaisyTheme(useThemeRuntime());
</script>
<template><slot /></template>The library's variables follow the active theme once the component mounts.
Adapter factory subpaths
Documents adapter factory subpaths
@themvora/shadcn/factory, -bootstrap/factory, -daisyui/factory and -open-props/factory export the adapter with no Vue wrapper — no onMounted, no disposal. They accept a runtime, a store, a theme definition or a token group.
The adapter has to be installed outside a component: a plugin, a test, or a runtime built in a plain .ts module. During SSR this is the only option, since there is no mount hook to wait for.
import { createBootstrapAdapter } from "@themvora/bootstrap/factory";
import type { ThemeRuntime } from "@themvora/core";
export function installAdapters(runtime: ThemeRuntime) {
const handle = runtime.adapters.use(createBootstrapAdapter());
return () => handle.dispose();
}You own the handle. runtime.adapters.use() returns it and dispose() removes the adapter along with the styles it wrote.
Transition
transition is a reactive prop: change it in the template and the runtime picks up the new options for the next switch, without remounting the provider.
transition
Documents transition prop
Enables and configures the cross-fade applied when the theme changes — duration, easing, preset, animated properties.
Leave the default on; tune it when the default feels slow, or disable it for an app that must repaint instantly.
<template>
<ThemeProvider
default-theme="mint-light"
:transition="{ enabled: true, duration: 250, easing: 'ease-in-out' }"
>
<RouterView />
</ThemeProvider>
</template>Theme changes animate. Changing the option after mount changes the next transition without a remount.
runtime.store.set(theme, { suppressTransition: true })
Applies one theme change immediately, skipping the configured animation for that update only.
A print path, a screenshot, or a programmatic reset where animating would look like a glitch.
import { useThemeRuntime } from "@themvora/vue";
export function usePrintTheme() {
const runtime = useThemeRuntime();
return function applyForPrint() {
const light = runtime.themes.find((theme) => theme.name === "mint-light");
if (!light) return;
runtime.store.set(light, { suppressTransition: true });
};
}That change is instant; later changes animate again.
Components
Vue ships the scoped-theme component. The React-only pieces (ThemeModeButton, ThemeInspector) are not part of this package — build them from the composables above.
ThemeScope
Applies a theme to a subtree: the wrapper carries the scoped CSS variables, so only that region repaints.
A preview panel, a marketing section in a fixed palette, or a code sample that must render in the theme it documents.
<script setup lang="ts">
import { ThemeScope } from "@themvora/vue";
</script>
<template>
<ThemeScope theme="cocoa-dark" class="rounded-lg p-6">
<h3>Always cocoa</h3>
<p>This panel keeps its own theme while the page changes.</p>
</ThemeScope>
</template>The global selection is neither read for the scope nor changed by it. A family scope follows the page mode; an exact theme stays pinned.
Relatedvue-use-theme
4A settings panel in Composition API
A complete appearance settings panel
Family, mode, and history controls in one component — each reading only the refs it needs, so a token change does not re-render the whole panel.
<script setup lang="ts">
import { useEffectiveThemeMode, useProvidedThemes, useTheme, useThemeHistory } from "@themvora/vue";
const MODES = ["light", "dark", "system"] as const;
const families = useProvidedThemes();
const { family, setFamily, mode, setMode } = useTheme();
const effectiveMode = useEffectiveThemeMode();
const { undo, canUndo } = useThemeHistory();
function handleFamilyChange(event: Event) {
const target = event.currentTarget;
if (!(target instanceof HTMLSelectElement)) return;
setFamily(target.value);
}
</script>
<template>
<form>
<label>
Family
<select :value="family" @change="handleFamilyChange">
<option v-for="entry in families" :key="entry.name" :value="entry.name">
{{ entry.name }}
</option>
</select>
</label>
<fieldset>
<legend>Mode</legend>
<label v-for="option in MODES" :key="option">
<input
type="radio"
name="mode"
:checked="mode === option"
@change="setMode(option)"
/>
{{ option }}
<span v-if="option === mode && mode === 'system'"> (now {{ effectiveMode }})</span>
</label>
</fieldset>
<button type="button" :disabled="!canUndo" @click="undo">Undo last change</button>
</form>
</template>Install a generated brand theme at runtime
A seed colour becomes a light/dark pair, the pair is registered, and the family is selected — batched so the page never paints the half-applied state.
<script setup lang="ts">
import { useThemeBatch, useThemePacks, useThemeRuntime } from "@themvora/vue";
import { generateTheme } from "@themvora/core";
const props = defineProps<{ seed: string }>();
const usePack = useThemePacks();
const batch = useThemeBatch();
const runtime = useThemeRuntime();
function applyBrand() {
const generated = generateTheme({ seed: props.seed, family: "brand" });
batch(() => {
usePack({ themes: [generated.light, generated.dark] });
runtime.selection.setFamily("brand");
});
}
</script>
<template>
<button @click="applyBrand">Use {{ seed }}</button>
</template>Not available in this integration
These exports exist in other integrations but not in @themvora/vue. Listing them is more useful than inventing an example: if you need one of them here, the workaround is named.
Vue does not export it. useThemeHistory() gives you history as a ref and jump(index) as an action, which covers the same ground.
React-only components. The composables in this package are enough to build the equivalent control; the inspector has no Vue counterpart.