Build Universal Vue Component Library Vue Demi

Create Vue.js libraries that work seamlessly across Vue 2 and Vue 3 with a single codebase. Learn the essential patterns for universal library development.

What is Vue Demi?

Vue Demi (demi means "half" in French) is a developing utility that allows developers to write universal Vue libraries for Vue 2 and Vue 3 without worrying about users' installed versions. Created by Anthony Fu, the maintainer of VueUse, Vue Demi has become the foundation for many popular Vue libraries including VueUse, Headless UI, and others that need to support both major versions of Vue.

When Vue 3 was released, library maintainers faced a significant challenge: the framework introduced breaking changes in its core API, reactivity system, and component architecture. While Vue 2 remained widely used, especially in enterprise environments, new projects increasingly adopted Vue 3. This created a fragmented ecosystem where libraries had to choose which version to support or maintain separate branches for each version.

Vue Demi offers a fourth option: write once, run anywhere. By abstracting version-specific differences, it enables developers to create libraries that serve the entire Vue ecosystem from a single codebase, dramatically reducing maintenance overhead while maximizing potential user reach.

For teams building custom Vue.js solutions, Vue Demi provides an essential foundation for creating reusable, version-agnostic components that can be shared across projects and distributed to other developers.

How Vue Demi Works

Vue Demi works by creating aliases and shims that abstract away version-specific differences. When a library is installed in a project, Vue Demi automatically detects the installed Vue version and exports the appropriate API surface.

Under the hood, Vue Demi:

  1. Detects whether Vue 2 or Vue 3 is installed in the project
  2. Re-exports core Vue exports (reactive, ref, computed, etc.) from the detected version
  3. Provides compatibility shims for APIs that behave differently between versions
  4. Allows library authors to write code using a unified API

This approach means your library code stays the same regardless of which Vue version consumers have installed. Vue Demi handles the complexity of adapting to different Vue versions transparently, ensuring consistent behavior across the ecosystem.

For enterprise Vue applications, this compatibility layer is invaluable--it allows organizations to upgrade Vue versions incrementally while continuing to use third-party libraries built with Vue Demi.

Key Benefits of Using Vue Demi

Single Codebase

Maintain one codebase that works with both Vue 2 and Vue 3, eliminating duplicate work and potential inconsistencies between versions.

Automatic Detection

Vue Demi automatically detects the installed Vue version and adapts accordingly at runtime without any configuration needed.

Unified API

Use consistent APIs across both Vue versions without writing conditional logic or maintaining separate code paths.

Ecosystem Adoption

Used by major Vue libraries like VueUse, Headless UI, and many others in the Vue ecosystem.

Setting Up a Project with Vue Demi

Installation

To start using Vue Demi in your project, you first need to set up your development environment:

# For a new library project
npm init vue-demi my-component-library

# Or install Vue Demi as a dependency
npm install vue-demi

Configuring Build Tools

Vue Demi requires proper configuration in your build tool (Vite, Rollup, or webpack) to ensure it can resolve the correct Vue version at build time.

For Vite users, add this to your vite.config.js:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
 plugins: [vue()],
 resolve: {
 alias: {
 'vue-demi': 'vue-demi/lib/index.cjs'
 }
 }
})

For library mode builds, you may need additional configuration to ensure tree-shaking works correctly and that your library can be consumed by both Vue 2 and Vue 3 projects. When working with our Vue development team, we ensure proper build configuration is in place from the start.

For Rollup users, consider using the rollup-plugin-vue-demi plugin to handle the configuration automatically.

Writing Universal Components

Using Vue Demi's Reactive APIs

One of the most important aspects of writing universal Vue components is using Vue Demi's reactive API exports. These functions automatically use the appropriate implementation based on the installed Vue version.

import { ref, reactive, computed, watch } from 'vue-demi'

export function useCounter(initialValue = 0) {
 const count = ref(initialValue)
 const doubleCount = computed(() => count.value * 2)

 function increment() { count.value++ }
 function decrement() { count.value-- }

 return { count, doubleCount, increment, decrement }
}

Lifecycle Hooks

Vue Demi provides consistent lifecycle hook access across both Vue versions:

import { onMounted, onUnmounted, onBeforeMount } from 'vue-demi'

export function useWindowSize() {
 const width = ref(window.innerWidth)
 const height = ref(window.innerHeight)

 function handleResize() {
 width.value = window.innerWidth
 height.value = window.innerHeight
 }

 onMounted(() => window.addEventListener('resize', handleResize))
 onUnmounted(() => window.removeEventListener('resize', handleResize))

 return { width, height }
}

These composable patterns are the foundation of modern Vue.js component development, enabling clean, reusable logic that works across Vue versions. For a deeper dive into Vue 3 component architecture, see our definitive guide to Vue 3 components.

Handling Version-Specific Behavior

While Vue Demi abstracts most differences, some edge cases may require version-specific handling:

import { isVue2, isVue3 } from 'vue-demi'

export function createUniversalComponent(props) {
 if (isVue2) {
 // Vue 2 specific implementation
 return { props, template: '<div class="vue2">{{ message }}</div>' }
 }

 if (isVue3) {
 // Vue 3 specific implementation
 return { props, template: '<div class="vue3">{{ message }}</div>' }
 }
}

Providing/Injecting Across Versions

The provide/inject pattern works differently between Vue 2 and Vue 3. Vue Demi provides a unified API:

import { provide, inject } from 'vue-demi'

const ThemeSymbol = Symbol('theme')

export function provideTheme(themeName: string) {
 provide(ThemeSymbol, {
 themeName,
 toggleTheme() { /* ... */ }
 })
}

export function useTheme() {
 const theme = inject(ThemeSymbol)
 if (!theme) throw new Error('Theme not provided!')
 return theme
}

Using version detection sparingly and relying on Vue Demi's unified API by default will keep your code cleaner and more maintainable long-term.

Advanced Patterns

Universal Composable Functions

Composable functions are the building blocks of modern Vue libraries:

import { ref, onMounted, onUnmounted } from 'vue-demi'

export function useLocalStorage(key: string, initialValue: any) {
 const storedValue = ref(initialValue)

 function getFromStorage() {
 try {
 const item = window.localStorage.getItem(key)
 if (item) storedValue.value = JSON.parse(item)
 } catch (error) {
 console.warn(`Error reading localStorage key "${key}":`, error)
 }
 }

 function setToStorage(value: any) {
 try {
 window.localStorage.setItem(key, JSON.stringify(value))
 storedValue.value = value
 } catch (error) {
 console.warn(`Error setting localStorage key "${key}":`, error)
 }
 }

 onMounted(getFromStorage)

 return {
 value: storedValue,
 set: setToStorage,
 remove: () => {
 window.localStorage.removeItem(key)
 storedValue.value = initialValue
 }
 }
}

Universal Plugin System

If your library needs to provide plugins that install globally:

import { install as installDemi } from 'vue-demi'

const installedComponents = new Set()

export function createMyPluginLibrary(options = {}) {
 function install(app, ...args) {
 if (installedComponents.has(app)) return
 // Register global components and composables
 installedComponents.add(app)
 }

 return { install, version: '1.0.0' }
}

These patterns form the basis of reusable Vue.js component libraries that can be shared across projects and organizations. Building such libraries requires careful attention to web development best practices to ensure long-term maintainability.

Best Practices for Universal Libraries

Avoiding Version-Specific Features

When writing universal libraries, avoid using Vue 3-only features like:

  • Teleport (Vue 2 has a plugin alternative)
  • Composition API <script setup> specific syntax
  • New lifecycle hook names (beforeUnmount instead of beforeDestroy)

Handling Template Syntax Differences

Some template syntax differs between versions:

export const MyInput = {
 props: ['modelValue'],
 emits: ['update:modelValue'],
 template: `
 <input
 :value="modelValue"
 @input="$emit('update:modelValue', $event.target.value)"
 />
 `
}

Tree-Shaking Considerations

Ensure your library is tree-shakeable by using named exports:

// Good: Named exports for tree-shaking
export function useFeatureA() { /* ... */ }
export function useFeatureB() { /* ... */ }

// Avoid: Default export with everything bundled
export default {
 useFeatureA() { /* ... */ },
 useFeatureB() { /* ... */ }
}

TypeScript Support

Vue Demi includes TypeScript definitions that adapt to the detected Vue version:

import type { Ref, ComputedRef } from 'vue-demi'

export function useDebouncedValue<T>(value: Ref<T> | ComputedRef<T>, delay: number): Ref<T> {
 // Implementation
}

Following these best practices ensures your library integrates seamlessly with both Vue 2 and Vue 3 projects while maintaining optimal bundle sizes through proper tree-shaking. Proper TypeScript support also enables better developer experience for consumers of your library.

Publishing and Distribution

Package.json Configuration

Configure your package.json for dual distribution:

{
 "name": "my-vue-demi-library",
 "version": "1.0.0",
 "main": "dist/index.cjs.js",
 "module": "dist/index.esm.js",
 "types": "dist/index.d.ts",
 "exports": {
 ".": {
 "require": "./dist/index.cjs.js",
 "import": "./dist/index.esm.js"
 }
 },
 "peerDependencies": {
 "vue": ">=2.7.0"
 }
}

Build Script Considerations

Ensure your build process creates bundles compatible with both environments:

# Build for ES modules (Vue 3 friendly)
npx vue-tsc --declaration --emitDeclarationOnly --outDir dist
rollup -c rollup.config.js --format es

# Build for CommonJS (Vue 2 friendly)
rollup -c rollup.config.js --format cjs

Proper package configuration ensures your library can be consumed by any Vue project, whether they use Vite, webpack, or other bundlers. The exports field in package.json is particularly important for modern bundlers to resolve the correct bundle format.

Testing Universal Libraries

Setting Up Test Environment

Test your library against both Vue 2 and Vue 3:

// vite.config.ts for Vue 3 testing
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
 plugins: [vue()],
 test: {
 environment: 'jsdom',
 include: ['**/*.test.ts']
 }
})

Running Tests Against Both Versions

Use CI to run tests against multiple Vue versions:

# .github/workflows/test.yml
jobs:
 test:
 strategy:
 matrix:
 vue-version: ['2.7', '3.4']
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '20'
 - run: npm install
 env:
 VUE_VERSION: ${{ matrix.vue-version }}
 - run: npm test

Comprehensive testing across Vue versions is essential for libraries that claim universal compatibility. Automated testing in CI ensures regressions are caught early and your library continues to work as expected with each Vue version. This approach mirrors the quality assurance standards we apply to all client projects.

Real-World Use Cases

Building Universal UI Component Libraries

When building UI components that work across versions, consider:

  1. Props and Events: Use consistent prop definitions and event naming
  2. Slots: Support both named and scoped slots
  3. Transitions: Use Vue Demi's transition helpers
  4. Styles: Consider CSS-in-JS or scoped CSS approaches

Creating Utility Libraries

For utility libraries like VueUse, focus on:

  1. Composable Functions: Write pure, testable functions
  2. No Component Dependencies: Keep logic decoupled from UI
  3. Tree-Shaking: Export individual functions for optimal bundling
  4. Type Safety: Provide complete TypeScript definitions

Supporting Legacy Projects

Vue Demi enables you to:

  1. Migrate Gradually: Support both versions during transition
  2. Maintain Single Codebase: Reduce maintenance burden
  3. Serve Enterprise Clients: Many still run Vue 2

These use cases demonstrate why Vue Demi has become essential for the Vue ecosystem. Whether you're building custom software solutions for enterprise clients or open-source libraries, Vue Demi provides the compatibility foundation you need. Organizations looking to modernize their Vue applications while maintaining backward compatibility benefit greatly from this approach.

Conclusion

Vue Demi represents a significant advancement in the Vue ecosystem, enabling developers to create truly universal libraries that serve both Vue 2 and Vue 3 users. By abstracting version-specific differences and providing a consistent API, it eliminates the maintenance burden of maintaining separate codebases while maximizing the potential user base for your libraries.

As the Vue ecosystem continues to evolve, tools like Vue Demi will remain essential for library authors who want to provide excellent developer experiences across the entire Vue community. Whether you're building UI component libraries, utility functions, or complex application frameworks, Vue Demi provides the foundation you need to write once and deploy everywhere.

If you're looking to build robust, version-agnostic Vue libraries for your organization or contribute to the open-source ecosystem, our web development team has extensive experience with Vue.js architecture and library development. Contact us to discuss how we can help you create reusable Vue components that stand the test of time.

Frequently Asked Questions

Do I need to install Vue 2 or Vue 3 separately with Vue Demi?

No, Vue Demi automatically uses whichever Vue version is installed in the consumer's project. You don't need to specify Vue as a direct dependency in your library.

Can I use Vue 3 specific features with Vue Demi?

Vue Demi provides the common API surface between Vue 2 and Vue 3. For Vue 3-only features, you would need to use conditional logic with isVue3/isVue2 checks, though this should be avoided when possible.

Is Vue Demi still maintained for Vue 2 compatibility?

Vue Demi continues to support Vue 2.7 (the final Vue 2 version) and Vue 3, making it suitable for libraries that need to support both versions during migration periods.

How does Vue Demi affect bundle size?

Vue Demi adds minimal overhead since it primarily re-exports from the installed Vue version. The bundle size impact is negligible for most applications.

Can I use Vue Demi with Nuxt?

Yes, Vue Demi works with both Nuxt 2 (Vue 2 based) and Nuxt 3 (Vue 3 based). Configure your build tool appropriately for each environment.

Ready to Build Your Vue Component Library?

Our team of Vue.js experts can help you create universal libraries that work across Vue 2 and Vue 3 with clean, maintainable code.

Sources

  1. Vue Demi GitHub Repository - Official repository with complete documentation
  2. LogRocket: Build a universal Vue component library with Vue Demi - Comprehensive implementation guide
  3. Anthony Fu: Make Libraries Working with Vue 2 and 3 - Creator's blog post on motivation and implementation
  4. Made with Vue.js: Vue Demi - Community overview and project description