Nuxt 4
Nuxt 4 module with server-rendered theming, cookie sync, auto-imported composables, and config-driven options.
@themvora/nuxtNuxt 4v2.0.0StableSSR:YesZero-flash:Yes1Install
pnpm add @themvora/core @themvora/nuxt2Set up
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
// Nuxt 4: the app lives in app/, so "~" resolves this to
// app/assets/css/main.css (the file from the styling step).
css: ["~/assets/css/main.css"],
themvora: {
defaultTheme: "light",
},
});themvora.initialMode in nuxt.config.ts decides the starting mode, and themvora.defaultTheme picks the theme.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">
// useTheme is auto-imported by the module — no import statement needed.
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
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
// Keep your stylesheet, and add the scrollbar styles that ship with core
// (not the module) whenever scrollbar is enabled.
css: ["~/assets/css/main.css", "@themvora/core/scrollbar.css"],
themvora: {
defaultTheme: "mint-light",
initialMode: "system",
transition: { duration: 360, easing: "cubic-bezier(0.4, 0, 0.2, 1)" },
scrollbar: true,
},
});nuxt.config.ts under the themvora key. The module reads it and emits the pre-paint bootstrap, so nothing is repeated elsewhere.What's Available
@themvora/nuxt ships 12 exports in 4 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 Nuxt 4 examples.
Add the module
Register the module and configure themes in nuxt.config.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
defaultTheme: "mint-light",
initialMode: "system",
},
});SSR-first theming
Cookies are validated (fingerprint) and the initial theme is resolved and rendered server-side with a blocking bootstrap — no flash of the wrong theme.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
defaultTheme: "mint-light",
initialMode: "system",
initialFamily: "mint",
},
});
// The module emits, before first paint:
// <html data-theme="mint-light" data-theme-mode="light" data-theme-family="mint">
// <style>:root{--theme-color-background:…}</style>
// <script>/* blocking bootstrap */</script>Auto-imported composables
useTheme and friends are auto-imported in every component.
<script setup lang="ts">
const { theme, mode, setMode, toggleTheme } = useTheme();
</script>
<template>
<button @click="toggleTheme">{{ theme.name }} · {{ mode }}</button>
<button @click="setMode('dark')">Dark</button>
</template>Components + runtime access
ThemeScope, ThemeScrollbar and the runtime are available everywhere.
<script setup lang="ts">
const runtime = useThemeRuntime();
function soften() {
// update() takes the tokens it is given; radius values are CSS lengths.
runtime.update({ radius: { sm: "8px", md: "12px" } });
}
</script>
<template>
<ThemeScope theme="plum-light">
<DataViz />
</ThemeScope>
<ThemeScrollbar :behavior="{ autoHide: true }" />
<button @click="soften">Soften corners</button>
</template>Runtime access in a plugin
The plugin-provided runtime is available as nuxtApp.$themvora.
// plugins/theme.client.ts
export default defineNuxtPlugin((nuxtApp) => {
const runtime = nuxtApp.$themvora as ThemeRuntime;
runtime.lifecycle.on("beforeThemeChange", (e) => {
console.log("theme changing to", e.next.name);
});
});Smooth theme transitions
Enable config-driven CSS transitions on theme changes.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
defaultTheme: "light",
transition: { enabled: true, duration: 360, easing: "cubic-bezier(0.4, 0, 0.2, 1)" },
},
});History controls in a component
The module auto-imports everything the component needs, so the file below is the component and nothing else.
<script setup lang="ts">
// Both composables and both components are auto-imported by the module.
const { undo, redo, canUndo, canRedo } = useThemeHistory();
const { theme, setFamily } = useTheme();
</script>
<template>
<ThemeScope theme="plum-dark" :transition="{ duration: 300, easing: 'ease' }">
<p>This subtree is always plum-dark.</p>
</ThemeScope>
<button @click="undo" :disabled="!canUndo">Undo</button>
<button @click="redo" :disabled="!canRedo">Redo</button>
<button @click="setFamily('mint')">{{ theme.name }}</button>
<ThemeScrollbar :behavior="{ autoHide: true }" />
</template>Undo, redo and the two can-do refs, with no import statements — the module supplies them.
The complete runnable app behind this guide is examples/apps/nuxt — listed with the other example apps.