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.

Create a free account to save this page, take notes and track your progress. Sign in

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 useColorMode implements the same behaviour.
  • In Nuxt, @nuxtjs/color-mode handles 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

Was this page helpful?