Learn Vue.js directives with this guide. Understand their role, learn to create custom directives, and discover practical use cases and the best practices of using them in your projects.
Abdelrahman Awad
July 9, 2025
Directives are a powerful tool in Vue.js, enabling the creation of declarative logic on DOM elements.
First off, what is a directive?
To refresh your memory, directives are special attributes in Vue.js templates that begin with a v-
prefix. Vue.js already provides a few built-in directives that do a lot of the heavy lifting for us.
Like v-if
which renders markup conditionally, or v-for
which loops through an array and renders markup for each item. There are more built-in directives and you can read about them in the Vue.js documentation.
We are not limited by the built-in directives that Vue.js provides. We can create our own custom directives and use them in our templates.
The simplest way to create a custom directive is by defining a function within your component.
<script setup>
function vGreet (el) {
el.innerText = 'Hello World'
}
</script>
<template>
<div v-greet></div>
</template>
Directive functions receive the element they are bound to as the first argument, so you can manipulate the element and its attributes.
When using directives like this, your functions must start with the v
prefix. This is a convention that Vue.js uses to distinguish directives from regular functions.
Directives also can be registered globally, so you can use them in any component. You can do so by calling app.directive
in your main entry point file, in many cases it's main.js
.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
function vGreet (el) {
el.innerText = 'Hello World'
}
// Register the directive globally
app.directive('greet', vGreet)
app.mount('#app')
In addition to receiving the element they are bound to, directives can also accept additional arguments, most notably the binding
object as the second argument.
binding
is extremely useful to make your directives more re-usable. The binding
object contains several properties that can be used to make your directives more flexible. Let's start with the value
property.
Keep in mind that directives are special attributes, and like any attribute, they can have values. The binding.value
property is the "attribute value" of the directive. But unlike regular attributes, the directive value is not a string, it's a JavaScript expression.
For example, if we were to have v-greet="name"
, the binding.value
would be the value of the name
variable, so you can do things like string concatenation:
<script setup>
let name = 'John'
function vGreet (el, binding) {
el.innerText = `Hello ${binding.value}`
}
</script>
<template>
<div v-greet="name + '!'"></div>
</template>
However if you just want to pass a string, then you need to make the expression a string.
<template>
<div v-greet="'John'"></div>
</template>
Notice the additional single quotes around the string. Again, because the directive value is an expression that will evaluate to a value.
Another interesting aspect of the binding
object is its modifiers
property. Just like the name implies, modifiers can be used to configure the directive and make it more flexible.
The modifiers
property is an object that contains the present modifiers applied to the directive, if a modifier is applied it will have a true
value.
In our greet directive example, we could add a formal
modifier to the directive to make it greet the user in a more formal way.
<script setup>
function vGreet (el, binding) {
el.innerText = `${binding.modifiers.formal ? 'Greetings' : 'Hello'} ${binding.value}`
}
</script>
<template>
<div v-greet.formal="'John'"></div>
</template>
You can have as many modifiers as you want, we could add a bold
modifier to make the text bold:
<script setup>
function vGreet (el, binding) {
const text = `${binding.modifiers.formal ? 'Greetings' : 'Hello'} ${binding.value}`
el.innerText = text;
if (binding.modifiers.bold) {
el.style.fontWeight = 'bold'
}
}
</script>
<template>
<div v-greet.formal.bold="'John'"></div>
</template>
Directives can also receive one argument, if any it becomes available as the binding.arg
property.
<script setup>
function vGreet (el, binding) {
let tag = binding.arg || 'span'
el.innerHTML = `<${tag}>Hello ${binding.value}</${tag}>`
}
</script>
<template>
<div v-greet:h2="'Everyone'"></div>
</template>
You probably noticed that the arg
is always a string, unlike the value
which is evaluated. You could pass a dynamic argument by wrapping the argument in square brackets, like this:
<script setup>
import { ref } from 'vue'
const tag = ref('pre');
function vGreet (el, binding) {
// ...
}
</script>
<template>
<div v-greet:[tag]="'Everyone'"></div>
<button @click="tag = tag === 'h2' ? 'pre' : 'h2'">Change tag</button>
</template>
Now the value of the arg
will be equal to the value of the tag
ref. You can place simple expressions inside the square brackets and they will be evaluated as well, so you have a lot of flexibility here.
There are more properties on the binding
object that you could use for advanced use-cases but for most cases, the value
, modifiers
and arg
properties will be your bread and butter.
So far, we've only seen the function version of a directive, but a directive can have multiple hook functions that you can use to have fine grained control over the directive's behavior. All of which are optional, but you have to have at least one in order for the directive to do something.
The full shape of a directive with all of its hooks is as follows:
const myDirective = {
// called before bound element's attributes
// or event listeners are applied
created(el, binding, vnode) {},
// called right before the element is inserted into the DOM.
beforeMount(el, binding, vnode) {},
// called when the bound element's parent component
// and all its children are mounted.
mounted(el, binding, vnode) {},
// called before the parent component is updated
beforeUpdate(el, binding, vnode, prevVnode) {},
// called after the parent component and
// all of its children have updated
updated(el, binding, vnode, prevVnode) {},
// called before the parent component is unmounted
beforeUnmount(el, binding, vnode) {},
// called when the parent component is unmounted
unmounted(el, binding, vnode) {}
}
The single function we've used so far serves as both the mounted
and updated
hooks.
Our simple example doesn't really need any of these hooks, but they can become valuable if your directive does something more complex or has performance implications.
This is a fun question, and the answer is yes, directives can be applied to components.
<template>
<MyComponent v-bold />
</template>
But, this has a few caveats.
First the el
argument will be the root element of the component, not the component itself. This means if your component has no root element, the directive won't work and it will throw a warning:
Runtime directive used on component with non-element root node. The directives will not function as intended.
It's good to know that Vue.js got your back in such cases.
This behavior cements the idea that directives ONLY operate on DOM elements.
Having explored what directives are and how they function, let's consider when to use them.
Since directives are only meant to be used on DOM elements, this means you should use them when whatever you want to do requires DOM manipulation or DOM APIs. Another limiting factor is that directives are stateless, meaning they do not have instance-specific state like components do.
With that in mind, directives can still be used for complex use-cases. Here are some examples:
Usually when you want access to an element in the template and you need to call a DOM API on it, you have to use a template ref.
For one offs, this can be tedious to define a template ref for each element you need to access. So instead, if your use-case is relatively simple, you can use a directive to call the DOM API.
A famous example is focusing elements, you can use a directive to focus an element when it is mounted.
<script setup>
function vFocus (el) {
el.focus()
}
</script>
<template>
<input v-focus />
</template>
In order to understand the user journey, you need to track the events they perform in the page. Typically you would use an analytics tool or a tracking SDK for it, which can be tedious to add to your every event handler around the app.
You could use a directive to track the events and send them to your analytics tool, this is a good example of a globally registered directive as well.
<button v-track:buy="{ amount: 100, currency: 'USD' }">Add to cart</button>
Tooltips are an interesting use-case of a directive and it can be quite complex to implement. But tooltips are often sprinkled around your app, and usually setting it up as a component is tedious because of the markup changes you need to make.
Luckily, there are libraries already that use directives to implement tooltips, like floating-vue.
<button v-tooltip="'You have ' + count + ' new messages.'">
...
</button>
Another thing that is similar to tooltips is paywalls, you could use a directive to show a paywall when the user tries to access a certain page when they are not on an eligible plan.
So it feels like whenever there is a behavior that needs to be "sprinkled" around the app, a directive could be a good fit.
We already have a couple of built-in directives that are useful for formatting and rendering content, like v-text
and v-html
. One thing to keep in mind is the elements they are usually used on have no content, so the same rule applies here to any custom directive we would design.
You could design a directive that formats a number value as a currency, like this:
<script setup>
function vCurrency(el, binding) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: binding.arg || 'USD'
});
el.innerText = formatter.format(binding.value);
}
</script>
<template>
<div v-currency="1000"></div>
</template>
You could also render content, one example is rendering markdown content, you could use a directive to render markdown content like this:
<script setup>
import { marked } from 'marked';
const md = `
# Hello World
This is a nice markdown content.
- List item 1.
- List item 2.
- List item 3.
`;
function vMarkdown(el, binding) {
el.innerHTML = marked(binding.value);
}
</script>
<template>
<div v-markdown="md"></div>
</template>
Let's wrap up with some best practices and recommendations I have picked up from my experience with Vue.js directives over the years.
Some of the directives we discussed require adding event listeners to the element, like the analytics tracking directive. One thing to keep in mind is that the mounted
and the created
hooks are only called once, so they are best suited for adding event listeners as you probably don't want to keep adding the listener whenever the directive binding changes.
Keep in mind that created
and beforeMount
hooks while can be used to add event listeners, the element is not in the DOM yet, so make sure you aren't relying on the element position or if you plan to do some DOM traversal. I usually stick to mounted
hook for setting up directives.
Modifiers are a great way to make your directives more flexible, but always treat them as optional. So if applicable, make sure to have a default value for the modifier.
function vCurrency(el, binding) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
// ⌠assuming 'arg' will always be provided is a bad idea
currency: binding.arg
// ✅ if the arg is missing, use a default value
currency: binding.arg || 'USD'
});
//...
};
If you absolutely need to have an argument, make sure to throw an explicit error if the argument is missing to make it easier to debug later. Similar recommendations for the modifiers.
Directives are a powerful tool in Vue.js and they can be used to create declarative logic on DOM elements. They are a great way to make your templates more expressive and to avoid writing too much logic in your components.
I hope this article has provided you with a comprehensive overview of what directives are, how they work, and how you can utilize them in your projects.
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.
Nuxt 4 is coming out, what does it mean?
Nuxt 4 (alpha) launched in June 2025 with a stable release on the horizon — bringing a smooth, stress-free upgrade path from Nuxt 3. Unlike past painful rewrites, Nuxt 4 builds on a solid foundation with improved developer experience, and it’s worth migrating now before Nuxt 5 arrives.
July 9, 2025
Reza Baar
Component Purity and StrictMode in React
Learn why keeping React components pure is essential for predictable behavior. Discover how StrictMode helps catch side effects and follow best practices for writing clean, maintainable components.
July 9, 2025
Aurora Scharff
Understanding Vue.js Directives
Learn Vue.js directives with this guide. Understand their role, learn to create custom directives, and discover practical use cases and the best practices of using them in your projects.
July 9, 2025
Abdelrahman Awad
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.