Vue.js Custom Directives

Vue's built-in directives — v-if, v-for, v-bind — cover most needs. When you need to interact directly with a DOM element in a reusable way, you create a custom directive. A custom directive runs JavaScript code on a specific element at precise lifecycle moments.

What Custom Directives Are For

Custom directives are for low-level DOM manipulation — tasks that cannot be done cleanly through data binding or components alone.

Good uses for custom directives:

  • Auto-focus an input when it appears
  • Scroll a container to a specific position
  • Attach a third-party library to a DOM element
  • Observe when an element enters the viewport
  • Apply a tooltip to any element

Registering a Global Custom Directive

Use app.directive() to register a directive that works anywhere in your app. The name you provide is what you write in templates, prefixed with v-.

const app = Vue.createApp({});

// Directive name: "focus"  → used as: v-focus
app.directive("focus", {
  mounted(el) {
    el.focus();
  }
});

app.mount("#app");
<!-- Use in template -->
<input v-focus type="text" placeholder="This gets focus automatically">

When this input appears on the page, Vue calls the mounted hook and runs el.focus() — no extra code in your component needed.

Directive Lifecycle Hooks

A custom directive can define functions for each stage of an element's life. You rarely need all of them — most directives use only one or two.

app.directive("my-directive", {
  beforeMount(el, binding) {
    // Element exists in DOM but not yet inserted into the page
  },
  mounted(el, binding) {
    // Element has been inserted into the page ← most common
  },
  beforeUpdate(el, binding) {
    // Component data changed, DOM about to re-render
  },
  updated(el, binding) {
    // DOM re-rendered with new data ← second most common
  },
  beforeUnmount(el, binding) {
    // Element about to be removed
  },
  unmounted(el, binding) {
    // Element removed from DOM — clean up here
  }
});

Diagram: Directive Hook Sequence

v-if becomes true → element inserted
  beforeMount() → mounted()

Bound data changes
  beforeUpdate() → updated()

v-if becomes false → element removed
  beforeUnmount() → unmounted()

The Binding Object

The binding parameter gives your directive information about how it was used — the value, any modifiers, and the argument.

app.directive("color", {
  mounted(el, binding) {
    el.style.color = binding.value;
  }
});
<p v-color="'red'">This text is red.</p>
<p v-color="userColor">This text uses a variable color.</p>

Binding Object Properties

<p v-example:title.bold.large="myValue">

binding.value       → myValue          (the bound data)
binding.arg         → "title"          (text after the colon)
binding.modifiers   → { bold: true, large: true }
binding.oldValue    → previous value (in update hooks only)

Diagram: Binding Object Anatomy

v-color:background.opacity="'#3498db'"

  Directive name: color
  Argument:       background
  Modifiers:      { opacity: true }
  Value:          "#3498db"

In the hook:
  binding.arg         → "background"
  binding.modifiers   → { opacity: true }
  binding.value       → "#3498db"

Practical Example: v-highlight

A directive that highlights an element with a background color. The color comes from the directive's value.

app.directive("highlight", {
  mounted(el, binding) {
    el.style.backgroundColor = binding.value || "yellow";
    el.style.padding = "2px 6px";
    el.style.borderRadius = "4px";
  },
  updated(el, binding) {
    el.style.backgroundColor = binding.value || "yellow";
  }
});
<p v-highlight="'#ffd700'">This text has a gold background.</p>
<p v-highlight>This text gets the default yellow background.</p>
<p v-highlight="userPickedColor">Dynamic color from data.</p>

Practical Example: v-tooltip

app.directive("tooltip", {
  mounted(el, binding) {
    el.setAttribute("title", binding.value);
    el.style.cursor = "help";
  },
  updated(el, binding) {
    el.setAttribute("title", binding.value);
  }
});
<button v-tooltip="'Save your changes'">Save</button>
<span v-tooltip="helpText">?</span>

Practical Example: v-click-outside

A common UI need — close a dropdown or modal when the user clicks anywhere outside of it.

app.directive("click-outside", {
  mounted(el, binding) {
    el._clickOutsideHandler = (event) => {
      if (!el.contains(event.target)) {
        binding.value(event);   // call the function passed as value
      }
    };
    document.addEventListener("click", el._clickOutsideHandler);
  },
  unmounted(el) {
    document.removeEventListener("click", el._clickOutsideHandler);
  }
});
<div id="app">
  <div v-click-outside="closeMenu" class="dropdown">
    <button @click="menuOpen = !menuOpen">Menu</button>
    <ul v-if="menuOpen">
      <li>Profile</li>
      <li>Settings</li>
      <li>Logout</li>
    </ul>
  </div>
</div>

<script>
  const app = Vue.createApp({
    data() { return { menuOpen: false }; },
    methods: {
      closeMenu() { this.menuOpen = false; }
    }
  });
  // register v-click-outside directive
  app.mount("#app");
</script>

Diagram: v-click-outside Logic

Dropdown is open.

User clicks INSIDE the dropdown:
  el.contains(event.target) → true
  binding.value NOT called → menu stays open ✓

User clicks OUTSIDE the dropdown:
  el.contains(event.target) → false
  binding.value() called → closeMenu() runs → menuOpen = false ✓

Local Directive in a Component

Register a directive locally inside a component using the directives option. It is only available within that component.

export default {
  directives: {
    autoscroll: {
      updated(el) {
        el.scrollTop = el.scrollHeight;
      }
    }
  },
  template: `<div v-autoscroll class="chat-log">...</div>`
};

Shorthand — Single Hook Directive

If your directive only needs the mounted and updated hooks with identical behavior, you can provide a single function as a shorthand.

// Instead of an object with hooks:
app.directive("pin", {
  mounted(el, binding) { el.style.position = "fixed"; el.style.top = binding.value + "px"; },
  updated(el, binding) { el.style.position = "fixed"; el.style.top = binding.value + "px"; }
});

// Use a function shorthand:
app.directive("pin", (el, binding) => {
  el.style.position = "fixed";
  el.style.top = binding.value + "px";
});
<div v-pin="20">Fixed 20px from top</div>

Summary

Custom directives give you direct, reusable access to DOM elements at the right lifecycle moment. Register them globally with app.directive() or locally in a component's directives option. Use the binding parameter to receive values, arguments, and modifiers from the template. The most commonly used hooks are mounted (element appears) and updated (data changes). Always clean up event listeners and timers in the unmounted hook. Custom directives are the right tool when you need to reach into the DOM directly in a way that components and data binding cannot cleanly express.

Leave a Comment

Your email address will not be published. Required fields are marked *