Custom Directives and Plugins

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Custom directives and plugins are Vue’s two extension points for logic that is not a component. A directive packages low-level DOM access you attach to an element with a v- attribute; a plugin is an object that installs app-wide features — components, directives, global properties, injected values — through a single app.use() call.

Custom directives

A directive is an object of lifecycle hooks that each receive the raw element. Inside <script setup>, any camelCase binding named vSomething is usable as v-something in that component’s template:

<script setup>
// registers v-focus for this component only
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

Use a directive when you genuinely need direct DOM manipulation that a binding or component cannot express — managing focus, integrating a non-Vue widget, custom scroll behaviour. See Custom Directives.

The hook set

Every hook is optional and receives (el, binding, vnode, prevVnode):

Hook Called

created

before the element’s attributes and event listeners are applied

beforeMount

before the element is inserted into the DOM

mounted

after the element and its parent component are mounted

beforeUpdate

before the containing component re-renders

updated

after the component and its children have updated

beforeUnmount

before the element is unmounted

unmounted

after the element has been removed

The list and each hook’s timing are in Directive Hooks.

Function shorthand

When the behaviour is the same on mounted and updated, pass a function instead of an object — it is registered as both hooks:

app.directive('color', (el, binding) => {
  el.style.color = binding.value   // runs on mounted AND updated
})

The binding object

The second argument carries the parsed template usage. For v-pin:right.far="offset":

Property Value

binding.value

the current value of the expression — here whatever offset holds

binding.oldValue

the previous value; available in beforeUpdate and updated only

binding.arg

"right" — can be dynamic, e.g. v-pin:[side]

binding.modifiers

an object of flags — here \{ far: true }

binding.instance

the component instance that is using the directive

app.directive('pin', (el, binding) => {
  el.style.position = 'fixed'
  const side = binding.arg || 'top'
  const offset = binding.modifiers.far ? '64px' : '16px'
  el.style[side] = offset
})
// <span v-pin:right.far="true">pinned</span>

Local vs. global registration

Global registration — app.directive('focus', \{ …​ }) in main.js — makes the directive available everywhere but invisible at the point of use. Prefer local registration: the vFocus object in <script setup>, or the directives option in an Options-API component:

// Options API -- local registration (contrast form)
export default {
  directives: {
    focus: { mounted: (el) => el.focus() }
  }
}

Directives on components

Discouraged. On a component with a single root node the directive applies to that root, like a fallthrough attribute; on a component with multiple root nodes it is ignored and Vue emits a warning. Prefer a prop or a wrapper element. See Usage on Components.

Plugins

A plugin is any object exposing an install(app, options) method (or a plain function used as that method). app.use(plugin, options) invokes it once and ignores repeat calls for the same plugin. Inside install you have the full application API — register components and directives, add global properties, or provide injectable values:

// plugins/i18n.js
export default {
  install(app, options) {
    const dict = options.messages ?? {}
    const translate = (key) => dict[key] ?? key

    // 1. a global property -- usable as $t in every template
    app.config.globalProperties.$t = translate

    // 2. an injectable value -- for inject('i18n') in <script setup>
    app.provide('i18n', { t: translate })

    // 3. ship a component and a directive with the plugin
    app.component('I18nText', {
      props: ['k'],
      setup: (props) => () => translate(props.k)
    })
    app.directive('t', (el, binding) => {
      el.textContent = translate(binding.value)
    })
  }
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './plugins/i18n.js'

createApp(App)
  .use(i18n, { messages: { hello: 'Hola', bye: 'Adios' } })
  .mount('#app')

Consume the provided value in a component:

<script setup>
import { inject } from 'vue'
const i18n = inject('i18n')
</script>

<template>
  <h1>{{ i18n.t('hello') }}</h1>
  <p v-t="'bye'"></p>
</template>

The install contract and app.use are documented in Plugins; providing values app-wide is app.provide; and to give $t a type on ComponentCustomProperties, see Augmenting Global Properties.

A toast plugin

A second, smaller example — a plugin that mounts its own host element and exposes an imperative push:

// plugins/toast.js
import { createApp, reactive } from 'vue'
import ToastHost from './ToastHost.vue'

export default {
  install(app) {
    const state = reactive({ items: [] })
    const push = (text) => state.items.push({ id: Date.now(), text })

    const mountPoint = document.createElement('div')
    document.body.appendChild(mountPoint)
    createApp(ToastHost, { state }).mount(mountPoint)

    app.provide('toast', { push })
    app.config.globalProperties.$toast = push
  }
}

Every mainstream Vue library follows this shape — app.use(pinia), app.use(router), app.use(i18n). See Routing for the router’s own app.use(router) call.

Mixins: legacy only

Before the Composition API, a mixin shared option fragments between components:

// legacy pattern -- avoid in new code
export const paginationMixin = {
  data: () => ({ page: 1 }),
  methods: { next() { this.page++ } }
}

export default {
  mixins: [paginationMixin]
}

Merged options hide where page and next came from, two mixins can silently collide on a name, and a mixin takes no arguments. The Vue documentation now points to composables for stateful reuse, custom directives for DOM logic, and plugins for app-level installation — see Composables vs. Mixins.

See also

  • Composables — the preferred mechanism for sharing stateful logic.

  • Routing — Vue Router installed as a plugin with app.use.

  • JavaScript Development — the module and closure semantics install and directive hooks rely on.