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.
