
A Vue 3 plugin is an object with an install(app, options) method, installed with app.use(). It registers components and directives, supplies values through app.provide() and configures the application. Vue 2 code built on Vue.prototype and Vue.mixin no longer works.
A Vue plugin adds capabilities to an entire application: a component available everywhere, a directive, an injectable value, shared configuration. It is the mechanism behind Vue Router, Pinia and most of the libraries in the ecosystem.
What a plugin is
An object with an install method, or a plain function. Vue calls it with the application instance and the options passed to app.use().
export default {
install(app, options) {
// everything happens here
}
}In Vue 2, install received the global Vue constructor, and a plugin worked on Vue.prototype, Vue.mixin or Vue.component. In Vue 3 there is no global constructor any more: install receives the instance created by createApp, and everything goes through it. A Vue 2 plugin copied across as-is fails with Cannot set properties of undefined (setting '$api'), or crashes silently.
Here is the mapping:
// Vue 3
Vue.use(Plugin) app.use(Plugin)
Vue.component('X', X) app.component('X', X)
Vue.directive('x', x) app.directive('x', x)
Vue.mixin({ … }) app.mixin({ … })
Vue.prototype.$api = api app.config.globalProperties.$api = api
app.provide('api', api) // preferredA complete plugin
The plugin below logs component mounts, exposes that log and provides a directive. It covers the four hooks worth knowing.
export default {
install(app, options = {}) {
const prefixe = options.prefixe ?? '[monitor]'
const journal = []
// 1. global property, reachable through this in the Options API
app.config.globalProperties.$journal = journal
// 2. injection: the recommended route with <script setup>
app.provide('journal', journal)
// 3. global directive
app.directive('surligne', {
mounted(el, binding) {
el.style.backgroundColor = binding.value ?? '#fef08a'
}
})
// 4. global mixin, run when each component is mounted
app.mixin({
mounted() {
journal.push(`${prefixe} ${this.$options.__name ?? 'anonyme'} monté`)
}
})
}
}Installing it, with or without options:
import { createApp } from 'vue'
import App from './App.vue'
import monitor from './plugins/monitor.js'
const app = createApp(App)
app.use(monitor, { prefixe: '[gekkode]' })
app.mount('#app')A plugin installed twice is only applied once: Vue keeps a register of the plugins already passed to app.use().
provide rather than globalProperties
app.config.globalProperties.$journal reproduces the Vue 2 habit of this.$something. It works, but this does not exist in <script setup>: the property is only reachable from the template or the Options API. It also escapes autocompletion and typing.
app.provide() is the right route. The component picks the value up with inject:
<script setup>
import { inject } from 'vue'
const journal = inject('journal')
</script>
<template>
<p>{{ journal.length }} composants montés</p>
</template>To avoid collisions between string names, use a Symbol exported by the plugin:
export const cleJournal = Symbol('journal')
export default {
install(app) {
app.provide(cleJournal, [])
}
}Shipping components
The most common case: making a library of components available without an import.
import GkBouton from '../components/GkBouton.vue'
import GkCarte from '../components/GkCarte.vue'
export default {
install(app, { prefixe = 'Gk' } = {}) {
app.component(`${prefixe}Bouton`, GkBouton)
app.component(`${prefixe}Carte`, GkCarte)
}
}Mind the trade-off: a globally registered component is available everywhere, but it escapes tree shaking. It ends up in the bundle even if nobody uses it. Keep global registration for the components that really are everywhere.
A plugin can be a plain function
If an object with an install method feels ceremonious, a function will do: Vue calls it with the same arguments.
export default function titrePlugin(app, options) {
app.config.globalProperties.$titre = options.titre
}The publishable package
Test it first in a fresh Vite project. To ship it on npm, export it as the default and keep the Vue dependency in peerDependencies, so that the host project supplies its own copy:
{
"name": "gekkode-monitor",
"type": "module",
"main": "./dist/index.js",
"exports": { ".": "./dist/index.js" },
"peerDependencies": { "vue": "^3.5.0" }
}Vue 2 plugins often ended with if (window.Vue) window.Vue.use(Plugin), so they could install themselves when the page loaded Vue through a script tag. There is no global window.Vue in Vue 3: that block does nothing and should be removed.
The global mixin, as a last resort
The example plugin uses app.mixin to watch every mount. That is the right tool for a cross-cutting need such as telemetry, where you genuinely want to reach every component.
For everything else, prefer a composable: an exported function that the interested components call. A global mixin applies everywhere, including where nobody expects it, and makes the origin of a behaviour hard to track down.
import { inject } from 'vue'
export function useJournal() {
const journal = inject('journal')
return {
journal,
ajouter: (message) => journal.push(message)
}
}Testing a plugin
Vue Test Utils takes plugins in the global option, along with their options:
import { mount } from '@vue/test-utils'
import monitor from '@/plugins/monitor.js'
mount(MonComposant, {
global: { plugins: [[monitor, { prefixe: '[test]' }]] }
})Without options, the short form is enough: plugins: [monitor].
Going further
Plenty of libraries in the ecosystem install this way, from VueFire to unhead for search engine optimisation.
Plugins make full sense once components and injection are familiar. If those notions are still new, go back to the Vue 3 tutorial for beginners, in particular the chapters on components and props. For shared state, Pinia is itself a plugin, and its source is worth reading.
Common errors
install receives the application instance, not the global constructor. Vue.prototype is gone: use app.provide() or app.config.globalProperties.this does not exist in <script setup>: the property is only readable from the template. Prefer provide and inject.if (window.Vue) window.Vue.use(Plugin) has nothing left to find in Vue 3 and should be removed.

