Nuxt 4 examples
Each export in @themvora/nuxt, with the problem it solves and the code that solves it.
@themvora/nuxtNuxt 4v2.0.0StableNuxt's integration is a module: themvora in nuxt.config.ts configures the registry, the server resolves the initial theme from cookies, and the client runtime is installed once as a plugin.
The convenience that matters is auto-imports — useTheme(), useThemeHistory(), ThemeScope and the rest need no import statement in a Nuxt app. Outside the auto-import scope, import them from @themvora/vue: Nuxt's import protection rejects a module's entry point in app code.
1The module renders the first byte
nuxt.config.ts. It registers the runtime plugin, auto-imports the composables and components, and emits the blocking bootstrap so the first paint is already themed.import { getBuiltInThemes } from "@themvora/core";
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
initialMode: "system",
},
});<script setup lang="ts">
// Auto-imported — no import statement needed.
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>With no themes option the module uses the built-in neutral themes, so an empty themvora: {} block is a complete setup.
2Module config, per-page overrides, honest labels
Guarantee a themed first byte
A Nuxt app flashes the default theme on reload and the switcher's label changes text after hydration.
SSR-first theme resolutionBlocking bootstrap scriptCookie + localStorage syncimport { getBuiltInThemes } from "@themvora/core";
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
initialMode: "system",
},
});The module resolves the cookie selection on the server and inlines the variables, so the HTML is already correct; the script only handles system with a dark-preferring visitor. Reloading with a persisted theme paints it immediately, with no hydration mismatch on components that render the theme name.
Watch out: Write the selection through the composables. Writing localStorage directly bypasses the cookie mirror and the server will render the previous theme.
Pin one page to its own theme
The pricing page must render in the brand theme regardless of the app-wide selection, without changing the visitor's preference.
ThemeScope (auto-imported)<template>
<ThemeScope theme="cocoa-light">
<h1>Pricing</h1>
<PricingTable />
</ThemeScope>
</template>ThemeScope writes the scoped variables onto its own wrapper and never touches the global selection. The page renders in the pinned theme; navigating away shows the previous selection unchanged.
Show the mode that is actually applied
The header icon says light while the page is dark, because the selection is system.
useTheme()<script setup lang="ts">
const { mode, effectiveMode } = useTheme();
</script>
<template>
<span :title="mode === 'system' ? 'Following the system' : undefined">
{{ effectiveMode === "dark" ? "🌙" : "☀️" }}
</span>
</template>useTheme() exposes effectiveMode alongside mode; the icon reads the first, and only a control that changes the selection needs the second. The icon matches the page in every case, including after a schedule transition.
3Module options and auto-imports
Server
The module's server half resolves the initial theme during the request and puts the result in the HTML. None of it is configured per page.
SSR-first theme resolution
Reads the theme-name, theme-mode, theme-family and theme-fingerprint cookies, validates the fingerprint against the current registry, resolves the initial theme, and renders <html data-theme> with the CSS variables inlined.
Always — it is what the module does. It matters most when the registry changed since the cookie was written, because the fingerprint rejects a stale selection instead of resolving a missing theme.
import { getBuiltInThemes } from "@themvora/core";
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
},
});The HTML that reaches the browser already carries the right theme; hydration attaches handlers to correct markup.
Blocking bootstrap script
A blocking <script> in <head> that re-applies the persisted selection before the body is parsed — the case the server cannot decide is system with a dark-preferring visitor.
Always, emitted by the module. It is what makes initialMode: "system" correct on the first frame.
<!-- Emitted by the module; you do not write this. -->
<script>
(function () {
var d = document.documentElement;
var mode = "system";
var prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
d.dataset.themeMode = mode === "system" ? (prefersDark ? "dark" : "light") : mode;
d.classList.toggle("dark", d.dataset.themeMode === "dark");
})();
</script>No flash for a visitor whose OS preference differs from the server's assumption.
Relatednuxt-dark-fallbacknuxt-ssr
Dark-mode CSS fallback
When the persisted mode is system, the server also emits an @media (prefers-color-scheme: dark) block carrying the dark theme's variables, so the browser can pick the palette without waiting for script.
Automatic. It covers browsers that apply the media query before any script runs.
:root {
--theme-color-background: #ffffff;
--theme-color-foreground: #0b0b0c;
}
@media (prefers-color-scheme: dark) {
:root {
--theme-color-background: #0b0b0c;
--theme-color-foreground: #f5f5f6;
}
}The light variables are emitted for the light scheme and the dark ones only inside the media query, so no inline value outranks the dark block.
Relatednuxt-bootstrapnuxt-schedule
Cookie + localStorage sync
The client mirrors the selection back to cookies so the server renders the right theme on the next request — the same contract as @themvora/next. localStorage is the browser copy; the cookies are what the server can read.
Automatic. It matters when you write a custom control: call the runtime's setters rather than writing localStorage yourself, or the server will render a stale theme.
<script setup lang="ts">
// The runtime mirrors this to cookies; writing localStorage directly would
// leave the server rendering the previous theme.
const { setFamily } = useTheme();
</script>A reload after a theme change is server-rendered in the new theme, with no correction frame.
Relatednuxt-ssrnuxt-runtime-plugin
Client
One plugin installs the runtime app-wide; the composables and components are auto-imported on top of it.
Runtime plugin
A Nuxt plugin installs one app-wide runtime and provides it to useTheme(), useThemeRuntime() and every auto-imported composable.
Always — the module registers it. You only interact with it when you need the runtime object itself.
<script setup lang="ts">
// Auto-imported from the module's plugin.
const runtime = useThemeRuntime();
</script>One runtime per app, shared by every component and by the module's own components. useThemeRuntime() returns it anywhere.
Relatednuxt-auto-importsnuxt-config
Auto-imports
Composables (useTheme, useThemeMode, useThemeHistory, …) and components (ThemeScope, ThemeScrollbar) are auto-imported, so a page needs no import statement. Outside the auto-import scope, import them from @themvora/vue.
Always. Use an explicit import in a plain .ts file outside the Nuxt auto-import scope, such as a test or a shared utility.
<template>
<ThemeScope theme="cocoa-dark" class="rounded-lg p-6">
<h3>Always cocoa</h3>
</ThemeScope>
</template>// The auto-import scope does not apply in a plain module. Import from the
// Vue integration — Nuxt's import protection rejects a module's entry point.
import { useThemeRuntime } from "@themvora/vue";Components resolve in templates without registration; composables resolve in setup without imports.
configKey: "themvora"
The module's options live under themvora in nuxt.config.ts: themes, defaultTheme, initialMode, initialFamily, transition, scrollbar, storageKey and scheduled.
Any app-level configuration. Because it is module config, it is fixed for the app rather than per page.
import { getBuiltInThemes } from "@themvora/core";
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
initialMode: "system",
initialFamily: "mint",
transition: { enabled: true, duration: 250 },
scrollbar: true,
storageKey: "app-theme",
},
});The server resolution, the blocking script and the client runtime all read the same options, so they cannot disagree.
Relatednuxt-ssrnuxt-transition
useThemeSchedule()
An auto-imported composable exposing the sunrise/sunset schedule: a reactive state (enabled, status, sunrise, sunset, nextTransition) plus enable(), disable() and set().
A settings row that turns daylight following on or off. Configure the schedule itself with scheduled in the module config.
<script setup lang="ts">
// Auto-imported. Destructure the ref: state is a Ref<ThemeScheduleState>.
const { state, enable, disable } = useThemeSchedule();
</script>
<template>
<label>
<input
type="checkbox"
:checked="state.enabled"
@change="state.enabled ? disable() : enable()"
/>
Follow daylight
<small v-if="state.nextTransition">
Next switch: {{ state.nextTransition.toLocaleTimeString() }}
</small>
</label>
</template>state updates as the sun moves; the methods write through to the runtime. Without scheduled in the config the state stays empty and the methods do nothing.
Transition
Configured once in the themvora block and handed to the plugin, so every page gets the same transition without repeating the option.
transition
Documents transition in nuxt.config.ts
Configure transition in the themvora config object to enable CSS transitions on theme changes. Duration and easing are set once; Themvora generates the transition styles at runtime.
Leave the default on; tune it when a large surface feels slow, or disable it for an app that must repaint instantly.
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
themvora: {
transition: { enabled: true, duration: 250, easing: "ease-in-out" },
},
});Theme changes cross-fade. Because the option is module config, changing it takes a config reload rather than a runtime call.
runtime.store.set(theme, { suppressTransition: true })
A per-update escape hatch that skips the configured animation for a single switch.
A print path, a screenshot, or a programmatic reset where animating would look like a glitch.
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 one change is instant; later changes animate again with the configured options.
Scrollbar
The overlay scrollbar has to exist before the first paint, so it is opted in on the server through the module config.
scrollbar: true
Enables Themvora's overlay scrollbar. The SSR bootstrap hides the native scrollbar before first paint, then the overlay engine synchronizes with browser scrolling without replacing native scroll behaviour.
A custom scrollbar is part of the design. Without the server half, the page paints a native bar for a frame.
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
// The overlay styles ship with core, not the module.
css: ["@themvora/core/scrollbar.css"],
themvora: { scrollbar: true },
});<template>
<!-- Auto-imported; the module config is what hides the native bar. -->
<ThemeScrollbar />
<NuxtPage />
</template>No native-bar flash; the overlay stays theme-aware and follows runtime theme changes.
scrollbar options
Documents scrollbar option
Configure the overlay appearance — thickness, radius, colours, auto-hide — from the themvora config instead of hand-written CSS.
The default overlay is too subtle or too heavy for the design, or the app needs an explicit colour token rather than the derived one.
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
css: ["@themvora/core/scrollbar.css"],
themvora: {
scrollbar: { thickness: 10, radius: 999, autoHide: true },
},
});The overlay is built from the options and stays theme-aware as the runtime theme changes.
Relatednuxt-scrollbar
4The complete module configuration
A complete module configuration
The options this page has referred to, in one place: registry, default, mode, transition, scrollbar and schedule.
import { getBuiltInThemes } from "@themvora/core";
export default defineNuxtConfig({
modules: ["@themvora/nuxt"],
// The scrollbar styles ship with core, not the module.
css: ["@themvora/core/scrollbar.css"],
themvora: {
themes: getBuiltInThemes(),
defaultTheme: "mint-light",
initialMode: "system",
initialFamily: "mint",
transition: { enabled: true, duration: 250 },
scrollbar: { thickness: 10, autoHide: true },
scheduled: { lightTheme: "mint-light", darkTheme: "mint-dark" },
storageKey: "app-theme",
},
});Not available in this integration
These exports exist in other integrations but not in @themvora/nuxt. Listing them is more useful than inventing an example: if you need one of them here, the workaround is named.
Neither belongs here: Nuxt owns the document and hydrates it, and Tailwind configuration is a CSS concern. Use @themvora/tailwind for the Tailwind mapping.