Dark Mode Without a Rebuild: Vuetify 3 Runtime Theming with CSS Variables
Vuetify 3 themes are CSS variables, not compiled SASS. Here's how to define light/dark palettes once, toggle them with useTheme, and keep custom components in sync.
20 Jan 2026, 01:01 UTC

Your users want a dark mode toggle, and you'd rather not ship two compiled stylesheets or force a page reload every time they flip it. Vuetify 3 solves this at runtime: theme colors live in CSS custom properties, so switching themes is a variable swap, not a rebuild.
The thesis is simple. If you define your light and dark palettes once in the plugin options and reference Vuetify's generated variables in your own CSS, the useTheme composable gives you instant, reactive theme switching across your whole app — including components you wrote yourself.
How Vuetify 3 theming actually works
Vuetify 2 compiled themes from SASS variables at build time. Vuetify 3 takes a different path: when the app boots, the framework generates a block of CSS custom properties — things like --v-theme-primary, --v-theme-background, and --v-theme-surface — and every Vuetify component reads its colors from those variables.
That architectural choice is what makes runtime switching cheap. Changing the active theme updates the variable values in the DOM, and the browser repaints everything that references them. No recompilation, no stylesheet swapping, no reload.
One consequence worth internalizing: these patterns do not transfer to Vuetify 2, and Vuetify 2 patterns won't work here. Everything below assumes Vuetify 3 with Vue 3.
Defining light and dark palettes once
Theme definitions live in the Vuetify plugin options, typically where you create the app (for example, src/plugins/vuetify.ts or wherever you call createVuetify):
import { createVuetify } from 'vuetify'
export default createVuetify({
theme: {
defaultTheme: 'light',
themes: {
light: {
colors: {
primary: '#1867C0',
background: '#FFFFFF',
surface: '#F5F5F5',
},
},
dark: {
colors: {
primary: '#5C9CE6',
background: '#121212',
surface: '#1E1E1E',
},
},
},
},
})Note the dark palette uses a lighter primary. Saturated brand colors often fail contrast checks against dark surfaces, so expect to adjust rather than reuse the same hex values verbatim.
Toggling with useTheme
Inside any component, the useTheme composable exposes the current theme and lets you change it. A minimal toggle button, runnable in any component's <script setup>:
<script setup lang="ts">
import { useTheme } from 'vuetify'
const theme = useTheme()
function toggle() {
theme.global.name.value =
theme.global.name.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<v-btn @click="toggle">Toggle theme</v-btn>
</template>Because theme.global.name is a ref, everything downstream reacts automatically. To verify it works, run the app, click the button, and inspect any Vuetify component in browser devtools — you should see the --v-theme-* values on the root element change between the two palettes.
Keeping your own components in sync
The underappreciated benefit of CSS-variable theming is that custom components can join in for free. Instead of hardcoding a color, reference the variable:
.status-card {
background: rgb(var(--v-theme-surface));
color: rgb(var(--v-theme-on-surface));
border-left: 4px solid rgb(var(--v-theme-primary));
}Vuetify emits the color variables as RGB triplets, which is why they're wrapped in rgb() — this also lets you do rgb(var(--v-theme-primary) / 0.5) for translucency. Check the exact variable format in devtools for your Vuetify version before relying on it, since naming details have shifted between minor releases.
Trade-offs and gotchas
Hardcoded colors silently break dark mode. Any custom CSS with literal hex values will not follow the theme. This is the most common source of "dark mode looks wrong" bugs, and it only shows up when someone actually toggles.
SSR needs care. If you server-render, the theme chosen on the client must match what the server rendered, or users see a flash of the wrong theme on hydration. Persisting the choice (for example, in localStorage or a cookie) and applying it before mount avoids the flicker, but cookie-based approaches are the reliable option for SSR since the server can read them.
Version sensitivity. Confirm the composable API and variable names against the docs for your installed Vuetify version — this area has evolved across 3.x releases.
Where to start
Pick one screen, move its custom colors to var(--v-theme-*) references, add the toggle button above, and flip between themes in devtools while watching the variables update. If every element on that screen follows the switch, your theming foundation is sound — and extending it to the rest of the app is mechanical work, not design work.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.