Understanding Vue.js Directives

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.

Abdelrahman Awad

Abdelrahman Awad

July 9, 2025

Understanding Vue Directives

Directives are a powerful tool in Vue.js, enabling the creation of declarative logic on DOM elements.

First off, what is a directive?

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.

Custom Directives

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')

Directive API

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.

Binding Value

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.

Binding Modifiers

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>

Binding Argument

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.

Directive Hooks

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.

Can they be applied to components?

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.

When to use a directive?

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:

DOM API Proxies

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>

Event Tracking

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

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.

Content Formatting and Rendering

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>

Best Practices

Let's wrap up with some best practices and recommendations I have picked up from my experience with Vue.js directives over the years.

Add listeners in stable hooks

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.

Do not require modifiers or an argument

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.

Conclusion

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.

More certificates.dev articles

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.

Looking for Certified Developers?

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.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)