Vue cookbook · Beginner
Implement a dark mode toggle that respects system settings
Users want light, dark or system theme, remembered across visits, without a flash of the wrong theme.
Approach
Keep the preference (light, dark, system) in a composable, resolve "system" with matchMedia, set a
data-theme attribute on <html>, and persist the choice. Apply the stored theme with a tiny inline script in
index.html before the app loads to avoid a flash.
Solution
// composables/useTheme.ts
import { computed, onScopeDispose, ref, watchEffect } from "vue";
type Preference = "light" | "dark" | "system";
const preference = ref<Preference>((localStorage.getItem("theme") as Preference) ?? "system");
const systemDark = ref(window.matchMedia("(prefers-color-scheme: dark)").matches);
export function useTheme() {
const media = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = (e: MediaQueryListEvent) => (systemDark.value = e.matches);
media.addEventListener("change", onChange);
onScopeDispose(() => media.removeEventListener("change", onChange));
const resolved = computed(() =>
preference.value === "system" ? (systemDark.value ? "dark" : "light") : preference.value);
watchEffect(() => {
document.documentElement.dataset.theme = resolved.value;
localStorage.setItem("theme", preference.value);
});
return { preference, resolved };
}
<script setup lang="ts">
import { useTheme } from "./composables/useTheme";
const { preference } = useTheme();
</script>
<template>
<label>
Theme
<select v-model="preference">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</template>
Pitfalls
- Only offering light/dark and ignoring the system setting.
- Applying the theme after the app mounts, causing a flash of the light theme.
- Insufficient contrast in the dark palette; check it like the light one.
Variations
- VueUse's
useColorModeimplements the same behaviour. - In Nuxt,
@nuxtjs/color-modehandles SSR and the no-flash script.
Further reading: prefers-color-scheme
Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
More Vue recipes
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch