Skip to content

VueJS - Directives

Directives are special tokens in the markup (v- prefix) that tell the library to do something to a DOM element. While Vue has many built-in directives (v-if, v-for, v-bind, v-model, v-on, etc.), you can also register your own custom directives for reusable DOM manipulation logic.

Custom directives are primarily intended for direct, low-level DOM manipulation when you need to encapsulate logic that doesn’t cleanly fit into components or composables.

You can register custom directives globally using app.directive() or locally within a component’s directives option or directly within <script setup>.

A directive definition object can provide several hook functions (all optional):

  • created(el, binding, vnode, prevVnode): Called before the element’s attributes or event listeners are applied.
  • beforeMount(el, binding, vnode, prevVnode): Called right before the element is inserted into the DOM.
  • mounted(el, binding, vnode, prevVnode): Called when the element is inserted into the DOM. Often used for setup that requires the element to be present (e.g., adding non-Vue event listeners, initializing a third-party library).
  • beforeUpdate(el, binding, vnode, prevVnode): Called before the element itself is updated (e.g., due to reactive data changes).
  • updated(el, binding, vnode, prevVnode): Called after the element and its children have updated.
  • beforeUnmount(el, binding, vnode, prevVnode): Called right before the element is removed from the DOM. Ideal for cleanup (e.g., removing event listeners, destroying library instances).
  • unmounted(el, binding, vnode, prevVnode): Called when the element has been removed from the DOM.
  • el: The DOM element the directive is bound to. You can directly manipulate this.
  • binding: An object containing properties like:
    • value: The value passed to the directive (e.g., v-my-directive="theValue").
    • oldValue: The previous value (only in beforeUpdate and updated).
    • arg: The argument passed (e.g., v-my-directive:foo).
    • modifiers: An object containing modifiers (e.g., v-my-directive.bar.baz).
    • instance: The instance of the component where the directive is used.
    • dir: The directive definition object.
  • vnode: The underlying virtual node representing the element.
  • prevVnode: The previous virtual node (only in beforeUpdate and updated).

Let’s create a simple v-focus directive that automatically focuses an input element when it’s mounted.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Custom Directive Example</title>
<script src="https://unpkg.com/vue@3"></script>
</head>
<body>
<div id="app">
<p>Click the button to mount the input field:</p>
<button @click="showInput = !showInput">Toggle Input</button>
<br><br>
<input type="text" v-if="showInput" v-focus placeholder="I should have focus when I appear">
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
const showInput = ref(false);
return { showInput };
}
});
// Register the custom directive globally
app.directive('focus', {
// Use 'mounted' hook, as the element needs to be in the DOM to be focused
mounted(el) {
console.log('v-focus mounted on:', el);
el.focus(); // Directly manipulate the DOM element
}
// Note: For components that might re-render, you might also need 'updated'
// if the element could potentially lose focus and needs it reapplied.
});
app.mount('#app');
</script>
</body>
</html>

In this example:

  • We define a global directive named focus using app.directive().
  • The mounted hook is used because focusing an element requires it to be present in the DOM.
  • Inside mounted, we simply call el.focus() to focus the input element.
  • When the input is conditionally rendered using v-if and appears on the page, the mounted hook of v-focus runs, and the input gets focus.

Example: Directive with Value and Argument

Section titled “Example: Directive with Value and Argument”

Let’s create a v-pin directive that positions an element fixed and pins it to a side of the screen based on an argument, with an optional top offset value.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 3 Directive with Value/Arg</title>
<script src="https://unpkg.com/vue@3"></script>
<style>
body { height: 200vh; } /* Ensure scrolling */
.pinned-element {
background-color: lightcoral;
padding: 15px;
border-radius: 5px;
color: white;
}
</style>
</head>
<body>
<div id="app">
<p>Scroll down to see the pinned elements.</p>
<!-- Pin to top, 20px offset -->
<div v-pin:top="20" class="pinned-element">
Pinned to Top (20px offset)
</div>
<!-- Pin to bottom, default 0px offset -->
<div v-pin:bottom class="pinned-element" style="background-color: lightseagreen;">
Pinned to Bottom (default offset)
</div>
</div>
<script>
const { createApp } = Vue;
const app = createApp({});
app.directive('pin', {
mounted(el, binding) {
console.log('v-pin mounted. Binding:', binding);
el.style.position = 'fixed';
// Get the argument (e.g., 'top' or 'bottom')
const side = binding.arg || 'top'; // Default to 'top' if no arg
// Get the value (the offset)
const offset = typeof binding.value === 'number' ? binding.value : 0; // Default to 0 if no value or not a number
// Apply style based on argument and value
el.style[side] = offset + 'px';
// Set opposite side based on the primary side to avoid width issues
if (side === 'top' || side === 'bottom') {
el.style.left = '10px'; // Add some horizontal positioning
el.style.right = '10px';
} else { // Could extend for left/right pinning too
el.style.top = '10px';
}
},
// Optional: Handle updates if the value/argument could change reactively
updated(el, binding) {
if (binding.value !== binding.oldValue || binding.arg !== binding.oldArg) {
console.log('v-pin updated. Binding:', binding);
const side = binding.arg || 'top';
const offset = typeof binding.value === 'number' ? binding.value : 0;
el.style[side] = offset + 'px';
}
}
});
app.mount('#app');
</script>
</body>
</html>

Here:

  • The v-pin directive uses the binding object.
  • binding.arg gives us the argument ('top' or 'bottom').
  • binding.value gives us the value passed (20 for the first div, undefined for the second).
  • We apply position: fixed and set the appropriate style (top or bottom) based on the argument and value.
  • An updated hook is added to handle potential reactive changes to the offset value or argument (though not demonstrated with reactive data in this simple example).

You can easily define directives locally within a component using <script setup>. Any camelCase variable starting with v is treated as a custom directive.

<script setup>
import { ref } from 'vue';
// Directive definition (must start with 'v')
const vHighlight = {
mounted: (el, binding) => {
el.style.backgroundColor = binding.value || 'yellow'; // Default to yellow
},
updated: (el, binding) => {
if (binding.value !== binding.oldValue) {
el.style.backgroundColor = binding.value || 'yellow';
}
}
};
const highlightColor = ref('lightgreen');
</script>
<template>
<p v-highlight="highlightColor">This text will be highlighted.</p>
<p v-highlight>This uses the default highlight color.</p>
<button @click="highlightColor = 'lightblue'">Change Highlight</button>
</template>

This approach keeps the directive logic colocated with the component that uses it, improving organization for directives not intended for global reuse.

Vue 2 had a feature called ‘Filters’ used for common text formatting within template expressions (e.g., {{ message | capitalize }}). Filters have been removed from Vue 3.

The recommended replacements for filters in Vue 3 are:

  • Computed Properties: For transformations based on reactive data.
  • Methods: For more complex formatting or formatting needed directly within event handlers or other methods.

Example: Replacing a ‘capitalize’ Filter

Section titled “Example: Replacing a ‘capitalize’ Filter”
<script setup>
import { ref, computed } from 'vue';
const message = ref('hello vue 3');
// Option 1: Computed Property
const capitalizedMessage = computed(() => {
if (!message.value) return '';
const firstChar = message.value.charAt(0).toUpperCase();
const rest = message.value.slice(1);
return firstChar + rest;
});
// Option 2: Method
function formatAsCurrency(value) {
if (typeof value !== 'number') return value;
return '$' + value.toFixed(2);
}
const price = ref(29.99);
</script>
<template>
<input v-model="message" />
<p>Original: {{ message }}</p>
<!-- Using computed property -->
<p>Capitalized (Computed): {{ capitalizedMessage }}</p>
<!-- Using method -->
<p>Price: {{ formatAsCurrency(price) }}</p>
</template>

Using computed properties or methods provides the same functionality as filters but integrates more naturally with standard JavaScript and Vue’s reactivity system.

Further Reading: Vue’s official documentation on Custom Directives: https://vuejs.org/guide/reusability/custom-directives.html