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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.