Custom Directives
What are Custom Directives?
Custom directives let you directly manipulate DOM elements in a reusable way. While components are for reusing UI logic, custom directives are for reusing low-level DOM manipulation — things like auto-focus, click-outside detection, tooltips, and lazy loading.
| Hook | When called |
|---|---|
created | Before element's attributes/event listeners are applied |
beforeMount | Before element is inserted into DOM |
mounted | After element is inserted into DOM |
beforeUpdate | Before the component updates |
updated | After the component and children update |
beforeUnmount | Before element is removed |
unmounted | After element is removed |
// directives/index.js
// v-focus — auto-focus an input
export const vFocus = {
mounted(el) {
el.focus()
}
}
// v-click-outside — detect clicks outside an element
export const vClickOutside = {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
if (!el.contains(event.target)) {
binding.value(event) // call the provided function
}
}
document.addEventListener('click', el._clickOutsideHandler)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutsideHandler)
}
}
// v-tooltip — show tooltip on hover
export const vTooltip = {
mounted(el, binding) {
const tooltip = document.createElement('div')
tooltip.className = 'tooltip'
tooltip.textContent = binding.value
tooltip.style.cssText = `
position: absolute; background: #333; color: white;
padding: 4px 8px; border-radius: 4px; font-size: 12px;
pointer-events: none; opacity: 0; transition: opacity 0.2s;
white-space: nowrap; z-index: 1000;
`
document.body.appendChild(tooltip)
el._tooltip = tooltip
el._showTooltip = () => {
const rect = el.getBoundingClientRect()
tooltip.style.left = rect.left + 'px'
tooltip.style.top = (rect.top - 30 + window.scrollY) + 'px'
tooltip.style.opacity = '1'
}
el._hideTooltip = () => { tooltip.style.opacity = '0' }
el.addEventListener('mouseenter', el._showTooltip)
el.addEventListener('mouseleave', el._hideTooltip)
},
updated(el, binding) {
el._tooltip.textContent = binding.value
},
unmounted(el) {
el.removeEventListener('mouseenter', el._showTooltip)
el.removeEventListener('mouseleave', el._hideTooltip)
el._tooltip.remove()
}
}
// v-lazy — lazy load images
export const vLazy = {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value
observer.disconnect()
}
}, { threshold: 0.1 })
observer.observe(el)
el._observer = observer
},
unmounted(el) {
el._observer?.disconnect()
}
}
// v-highlight — highlight text
export const vHighlight = {
mounted(el, binding) {
el.style.backgroundColor = binding.value || 'yellow'
},
updated(el, binding) {
el.style.backgroundColor = binding.value || 'yellow'
}
}
// Register globally in main.js:
// app.directive('focus', vFocus)
// app.directive('click-outside', vClickOutside)
// app.directive('tooltip', vTooltip)
<template>
<div>
<!-- v-focus — auto-focus on mount -->
<input v-focus placeholder="I'm auto-focused!" />
<!-- v-click-outside — close dropdown when clicking outside -->
<div class="dropdown" v-click-outside="closeDropdown">
<button @click="isOpen = !isOpen">Menu</button>
<ul v-if="isOpen">
<li>Option 1</li>
<li>Option 2</li>
</ul>
</div>
<!-- v-tooltip — show tooltip on hover -->
<button v-tooltip="'Click to save your changes'">Save</button>
<button v-tooltip="tooltipText">Dynamic tooltip</button>
<!-- v-lazy — lazy load image -->
<img v-lazy="'/images/large-photo.jpg'" alt="Lazy loaded" />
<!-- v-highlight with value -->
<p v-highlight="'#ffeb3b'">This text is highlighted yellow</p>
<p v-highlight="'#e3f2fd'">This text is highlighted blue</p>
<!-- Local directive (in <script setup>, prefix with v) -->
<input v-auto-resize />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { vFocus, vClickOutside, vTooltip, vLazy, vHighlight } from '@/directives'
const isOpen = ref(false)
const tooltipText = ref('Hello from Vue!')
function closeDropdown() {
isOpen.value = false
}
// Local directive — defined in <script setup>, no registration needed
// Just name it vSomething and use as v-something
const vAutoResize = {
mounted(el) {
el.style.resize = 'none'
el.style.overflow = 'hidden'
const resize = () => {
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
}
el.addEventListener('input', resize)
el._resize = resize
},
unmounted(el) {
el.removeEventListener('input', el._resize)
}
}
</script>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.