
Creating Custom Vue Directives for Enhanced Functionality

Custom directives sit in an interesting place in Vue. They are not the main tool we reach for every day, and that is a good thing. Most behaviour is better expressed through components, props, events, or composables. Directives are for the cases where we really do need low‑level DOM access.
That is exactly why they remain valuable. A well‑placed directive can capture a piece of DOM behaviour cleanly without forcing that concern into component setup logic over and over again.
When a Directive is the Right Tool
Directives are a good fit when the behaviour is tightly tied to a real DOM element:
- auto‑focus
- click‑outside handling
- intersection observer wiring
- imperative third‑party DOM integration
If the feature is mostly stateful application logic, a composable is usually the better abstraction. Vue's own guidance points in that direction, and it is sensible advice. Directives should exist because the browser element itself matters, not because we want another place to put code.
A Simple v‑focus Directive
The classic example is still a good one:
import type { Directive } from 'vue';export const vFocus: Directive<HTMLInputElement> = { mounted: (element) => { element.focus(); },};We can then register and use it in a component:
<script setup lang="ts">import { vFocus } from '@/directives/focus';</script><template> <input v-focus type="text" placeholder="Search articles" /></template>This is small, but it captures the core value of directives well. We are attaching low‑level DOM behaviour at the template level without polluting the component with imperative DOM code.
A More Realistic Example: v‑click‑outside
The place directives start to feel more useful is when the behaviour would otherwise be repeated across several menus, dropdowns, and popovers:
import type { Directive } from 'vue';type ClickOutsideElement = HTMLElement & { __clickOutsideHandler__?: (event: MouseEvent) => void;};export const vClickOutside: Directive< ClickOutsideElement, () => void> = { mounted: (element, binding) => { const handler = (event: MouseEvent): void => { if (!(event.target instanceof Node)) { return; } if (!element.contains(event.target)) { binding.value(); } }; element.__clickOutsideHandler__ = handler; document.addEventListener('mousedown', handler); }, unmounted: (element) => { const handler = element.__clickOutsideHandler__; if (handler) { document.removeEventListener('mousedown', handler); } },};Used in a component:
<template> <aside v-click-outside="closePanel"> <slot /> </aside></template>That is a much better directive use case. The behaviour is DOM‑specific, repeated in several places, and not really improved by being rewritten inside every consuming component.
Directive Hooks and Cleanup Matter
Directive hooks such as mounted, updated, and unmounted are where lifecycle discipline becomes important. If the directive adds listeners, observers, or third‑party instances, it should also clean them up properly.
That has direct implications for maintainability. Directives that leave behind listeners or hidden DOM mutations tend to become awkward bugs later, especially in larger component trees.
This is also where directive design becomes more revealing. A tiny mounted hook is easy to trust. A directive with a sprawling updated branch, multiple side effects, and no teardown usually means the abstraction is carrying more than it should.
Bindings, Arguments, and Boundaries
Another reason directives can get messy is that they support quite a lot of surface area. A directive can receive a value, arguments, and modifiers, which is useful, but also makes it easy to create a mini‑framework in the template if we are not careful.
That is why I would keep the contract narrow. If a directive needs six options, three modifiers, and complex business rules, the question is no longer "how do we write this directive?" It is "should this still be a directive at all?"
A lot of developers assume that directives are a more advanced or more powerful replacement for components. Usually they are not. They solve a narrower problem. Their job is direct DOM handling, not broad application architecture.
Directive vs. Composable
There is a practical distinction worth holding onto here:
- use a directive when the behaviour belongs to the host element
- use a composable when the behaviour belongs to reusable stateful logic
Click‑outside can live in either world depending on how we want to consume it. If the goal is to keep the template expressive with v‑click‑outside, a directive makes sense. If the goal is to share reactive state and callbacks across several interactions, a composable may be the better boundary.
The point is not that one is more modern than the other. The point is to choose the abstraction that matches where the problem actually lives.
Practical Advice
- Reach for a directive only when direct element‑level DOM work is genuinely required.
- Keep the directive small and single‑purpose.
- Clean up listeners and observers in teardown hooks.
- Avoid hiding business rules inside directives, because that makes debugging harder.
- Be suspicious of directives with very broad binding APIs.
For the Vue details behind the patterns discussed here, the official guides below are worth keeping nearby:
Wrapping up
Custom directives are most effective when we treat them as a focused escape hatch for DOM‑specific behaviour. Used that way, they can keep templates expressive and components cleaner. Used too broadly, they usually become harder to test and harder to reason about than the alternatives.
Key Takeaways
- Use custom directives for low‑level DOM behaviour, not general application logic.
- The best directive examples are usually small, DOM‑specific, and repeated across several components.
- Keep the API and lifecycle hooks small and explicit.
- Prefer components or composables whenever direct DOM access is not essential.
Once we apply that discipline, custom directives become a precise tool rather than an ambiguous abstraction.
Related Articles

Breadth‑First Search: Solving Binary Tree Level Order Traversal. 
Building Custom Directives in Angular. Building Custom Directives in Angular

Using the Modulo Operator in JavaScript. Using the Modulo Operator in JavaScript

Throttling Scroll Events in JavaScript. Throttling Scroll Events in JavaScript
Get the Number of Years Between Two Dates with PHP and JavaScript. Get the Number of Years Between Two Dates with PHP and JavaScript

Interpolation: Sass Variables Inside calc(). Interpolation: Sass Variables Inside
calc()
Understanding the Difference Between 'Indexes' and 'Indices'. Understanding the Difference Between 'Indexes' and 'Indices'
Setting CSS Blur Filter to Zero on a Retina Screen. Setting CSS Blur Filter to Zero on a Retina Screen

Understanding Transient Props in styled‑components. Understanding Transient Props in
styled‑components
Intervals in Practice: Solving the 'Merge Intervals' Problem. Intervals in Practice: Solving the 'Merge Intervals' Problem

Throttling vs. Debouncing in JavaScript: Managing Event Frequency. Throttling vs. Debouncing in JavaScript: Managing Event Frequency

Add Two Numbers in TypeScript: Linked Lists Without the Hand‑Waving. Add Two Numbers in TypeScript: Linked Lists Without the Hand‑Waving