Frontend Development 15 min read

How vueuse Supports Vue 2 & Vue 3 via vue‑demi and Implements Dark Mode

This article explains how the vueuse utility library works with both Vue 2 and Vue 3 by leveraging the vue‑demi compatibility layer, and then dives into the implementation of its useDark hook, showing the underlying media‑query logic and CSS techniques for dark‑mode support.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
How vueuse Supports Vue 2 & Vue 3 via vue‑demi and Implements Dark Mode

vueuse is a popular utility library built on Vue's Composition API. It works with both Vue 2 and Vue 3 by using the compatibility package vue-demi , which dynamically switches the underlying Vue version at install time.

The core of vue-demi is its postinstall script defined in package.json :

"scripts": {
  "postinstall": "node -e \"try{require('./scripts/postinstall.js')}catch(e){}\"",
},

During post‑install, the script detects the installed Vue version and rewrites the entry files in package.json to point to the appropriate Vue API implementation. The key function switchVersion copies version‑specific source files ( index.cjs , index.mjs , index.d.ts ) into the library root and, for Vue 2, updates the Composition API exports.

const { switchVersion, loadModule } = require('./utils')
const Vue = loadModule('vue')
if (!Vue || typeof Vue.version !== 'string') {
  console.warn('[vue-demi] Vue is not found. Please run "npm install vue" to install.')
} else if (Vue.version.startsWith('2.7.')) {
  switchVersion(2.7)
} else if (Vue.version.startsWith('2.')) {
  switchVersion(2)
} else if (Vue.version.startsWith('3.')) {
  switchVersion(3)
} else {
  console.warn(`[vue-demi] Vue version v${Vue.version} is not supported.`)
}

Because vue-demi simply re‑exports the native Vue 3 API (or a thin wrapper for Vue 2), developers can import hooks from vueuse without worrying about the underlying version:

import {
  watch,
  computed,
  Ref,
  ref,
  set,
  del,
  nextTick,
  isVue2,
} from 'vue-demi'

Beyond version compatibility, the article examines the useDark hook provided by vueuse . The hook builds on usePreferredDark , which itself is a thin wrapper around useMediaQuery('(prefers-color-scheme: dark)') . This media query leverages the browser's matchMedia API to detect whether the user prefers a dark color scheme.

export function usePreferredDark(options?: ConfigurableWindow) {
  return useMediaQuery('(prefers-color-scheme: dark)', options)
}

The useMediaQuery implementation creates a MediaQueryList , registers change listeners, and returns a reactive ref that reflects the current match state:

export function useMediaQuery(query, options = {}) {
  const { window = defaultWindow } = options
  const isSupported = useSupported(() => window && 'matchMedia' in window && typeof window.matchMedia === 'function')
  let mediaQuery
  const matches = ref(false)
  const handler = (event) => { matches.value = event.matches }
  const cleanup = () => {
    if (!mediaQuery) return
    if ('removeEventListener' in mediaQuery) mediaQuery.removeEventListener('change', handler)
    else mediaQuery.removeListener(handler)
  }
  const stopWatch = watchEffect(() => {
    if (!isSupported.value) return
    cleanup()
    mediaQuery = window.matchMedia(toValue(query))
    if ('addEventListener' in mediaQuery) mediaQuery.addEventListener('change', handler)
    else mediaQuery.addListener(handler)
    matches.value = mediaQuery.matches
  })
  tryOnScopeDispose(() => { stopWatch(); cleanup(); mediaQuery = undefined })
  return matches
}

The useDark hook combines this detection with a useColorMode manager, allowing developers to read or toggle the dark mode state and automatically sync it with the system preference.

export function useDark(options = {}) {
  const { valueDark = 'dark', valueLight = '', window = defaultWindow } = options
  const mode = useColorMode({
    ...options,
    modes: { dark: valueDark, light: valueLight },
    onChanged: (mode, defaultHandler) => {
      if (options.onChanged) options.onChanged?.(mode === 'dark', defaultHandler, mode)
      else defaultHandler(mode)
    },
  })
  const system = computed(() => {
    if (mode.system) return mode.system.value
    const preferredDark = usePreferredDark({ window })
    return preferredDark.value ? 'dark' : 'light'
  })
  const isDark = computed({
    get: () => mode.value === 'dark',
    set: (v) => {
      const modeVal = v ? 'dark' : 'light'
      mode.value = system.value === modeVal ? 'auto' : modeVal
    },
  })
  return isDark
}

In practice, the hook is used like this:

import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)

The article also mentions the CSS color-scheme property, which ensures that UI elements such as scrollbars adapt to the selected theme, and shows a simple CSS variable approach for swapping background and text colors between light and dark modes.

:root {
  --vp-c-bg: #ffffff;
  --vp-c-text-1: rgba(60, 60, 67);
}
.dark {
  --vp-c-bg: #1b1b1f;
  --vp-c-text-1: rgba(255, 255, 245, .86);
}
html.dark { color-scheme: dark; }

Finally, a complete Vue component example demonstrates toggling dark mode with a button, applying the .dark class to the html element, and using the defined CSS variables to change the page appearance.

frontendVueComposition APIdark modevue-demiVueUse
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.