Explore
Implementing Dark Mode in a Nuxt Application

Implementing Dark Mode in a Nuxt Application

Dark mode is a UI feature users actively want. Here's how to implement it correctly in Nuxt — respecting system preferences, persisting the choice, and avoiding flash-of-wrong-theme.

Implementing Dark Mode in a Nuxt Application

Dark mode seems simple: invert the colours. In practice, there are several ways to do it wrong — flash of wrong theme on page load, system preference ignored, preference not persisted across sessions, SVGs and images that break the illusion.

Here's the correct implementation in Nuxt with Tailwind.


The approach: CSS custom properties + Tailwind's darkMode: 'class'

Tailwind's dark mode via the class strategy is the most flexible approach. You add a dark class to the <html> element to activate dark mode; Tailwind applies dark: prefixed utilities only when this class is present.

// tailwind.config.ts
export default {
  darkMode: 'class',
  // ...
}

Then in your CSS, define colours as custom properties for both themes:

/* assets/css/main.css */
:root {
  --color-bg: #ffffff;
  --color-bg-secondary: #f9fafb;
  --color-text: #111827;
  --color-text-muted: #6b7280;
  --color-border: #e5e7eb;
}

.dark {
  --color-bg: #0f172a;
  --color-bg-secondary: #1e293b;
  --color-text: #f1f5f9;
  --color-text-muted: #94a3b8;
  --color-border: #334155;
}

The composable

// composables/useColorMode.ts
export type ColorMode = 'light' | 'dark' | 'system'

export function useColorMode() {
  const preference = useLocalStorage<ColorMode>('color-mode', 'system')

  const isDark = computed(() => {
    if (preference.value === 'system') {
      // Check system preference
      return window.matchMedia('(prefers-color-scheme: dark)').matches
    }
    return preference.value === 'dark'
  })

  function applyMode() {
    if (isDark.value) {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
  }

  // Apply on preference change
  watch(preference, applyMode)

  // Watch system preference changes
  onMounted(() => {
    applyMode()
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    mediaQuery.addEventListener('change', applyMode)
    onUnmounted(() => mediaQuery.removeEventListener('change', applyMode))
  })

  function setMode(mode: ColorMode) {
    preference.value = mode
  }

  return { preference, isDark, setMode }
}

Preventing flash of wrong theme

This is the hard part. If the user prefers dark mode and you apply it via JavaScript on onMounted, there's a brief flash of light mode before the script runs.

The solution: an inline script in the <head> that runs synchronously before the page renders.

In Nuxt, add this via app.head in your config:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          innerHTML: `
            (function() {
              var stored = localStorage.getItem('color-mode');
              var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
              if (stored === 'dark' || (stored !== 'light' && prefersDark)) {
                document.documentElement.classList.add('dark');
              }
            })();
          `,
          type: 'text/javascript'
        }
      ]
    }
  }
})

This runs before any CSS is applied, so the correct class is on <html> before the browser paints.


The toggle component

<!-- components/ColorModeToggle.vue -->
<script setup>
const { preference, setMode } = useColorMode()

function toggle() {
  setMode(preference.value === 'dark' ? 'light' : 'dark')
}
</script>

<template>
  <button
    @click="toggle"
    class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
    :aria-label="preference === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
  >
    <SunIcon v-if="preference === 'dark'" class="w-5 h-5" />
    <MoonIcon v-else class="w-5 h-5" />
  </button>
</template>

Using Nuxt Color Mode module

If you'd rather not implement this yourself, @nuxtjs/color-mode handles all of the above:

npx nuxi module add color-mode
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/color-mode'],
  colorMode: {
    classSuffix: '',       // Use 'dark' not 'dark-mode'
    preference: 'system',  // Default to system
    fallback: 'light'
  }
})

Usage:

<script setup>
const colorMode = useColorMode()
// colorMode.preference: 'system' | 'light' | 'dark'
// colorMode.value: 'light' | 'dark' (resolved)
</script>

The module handles the anti-flash script, localStorage persistence, and system preference detection automatically.


Testing dark mode

Check for:

  • Images with white backgrounds (jarring on dark backgrounds — use transparent PNGs or add a subtle background)
  • SVG icons with hardcoded fill="black" or fill="white" (use currentColor instead)
  • Third-party embeds (iframes, widgets) that don't respect your theme
  • Form autofill styles (browsers apply their own background colours which may not match)

Test in both modes with each release. Dark mode bugs are easy to miss if you only test the default.

Want dark mode done right from day one? We build that →