Directives
Vue ships a handful of built-in directives (v-if, v-for, v-model, v-show), and lets you register custom ones for low-level DOM manipulation that does not fit a component — autofocus, click-outside, intersection observers, third-party widget bootstrapping. Use them for cross-cutting DOM behaviour; reach for components for everything else.
Three custom directives: focus, click-outside, intersection
EXAMPLE
<script setup lang='ts'>
import { ref } from 'vue';
// 1) v-focus — focuses the element on mount
const vFocus = {
mounted: (el: HTMLElement) => el.focus(),
};
// 2) v-click-outside — emits a callback when the user clicks elsewhere
const vClickOutside = {
mounted(el: HTMLElement & { __handler?: (e: Event) => void }, binding: any) {
el.__handler = (e: Event) => {
if (!el.contains(e.target as Node)) binding.value(e);
};
document.addEventListener('click', el.__handler);
},
unmounted(el: HTMLElement & { __handler?: (e: Event) => void }) {
document.removeEventListener('click', el.__handler!);
},
};
// 3) v-visible — uses IntersectionObserver to fire once on first visibility
const vVisible = {
mounted(el: HTMLElement & { __io?: IntersectionObserver }, binding: any) {
el.__io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
binding.value(entry);
el.__io!.disconnect();
}
}, { threshold: binding.arg ? Number(binding.arg) : 0.25 });
el.__io.observe(el);
},
unmounted(el: HTMLElement & { __io?: IntersectionObserver }) {
el.__io?.disconnect();
},
};
const open = ref(true);
const onAway = () => (open.value = false);
const onSeen = (e: IntersectionObserverEntry) =>
console.log('seen:', e.target);
</script>
<template>
<input v-focus placeholder='I am focused on mount' />
<div v-if='open' v-click-outside='onAway' class='popover'>
Click outside this panel to dismiss it.
</div>
<!-- argument 0.5 = fire when 50% visible -->
<img v-visible:0.5='onSeen' src='/hero.jpg' alt='hero' />
</template>
<style>
.popover { padding: .75rem 1rem; border: 1px solid #ddd; background: #fff; }
</style>
Why it matters
Stash per-element state on the DOM node (el.__io, el.__handler) so unmounted() can clean it up — a forgotten listener is a real memory leak. If a directive needs reactive state, you are usually a step away from wanting a component instead.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…