A custom directive packages low-level behavior applied directly to an element. Use one when the concern is fundamentally about DOM lifecycle, such as focus management, intersection observation, or integrating a library that needs an element. Reusable stateful logic without direct element manipulation belongs in a composable.
Directives are special attributes with the v- prefix that apply reactive behavior to the DOM. They are Vue's way of extending HTML with dynamic functionality.
| Directive | Purpose | Shorthand |
|---|---|---|
| v-if / v-else-if / v-else | Conditional rendering (removes from DOM) | - |
| v-show | Toggle visibility (keeps in DOM) | - |
| v-for | Render list | - |
| v-bind | Bind attribute to data | : |
| v-on | Listen to events | @ |
| v-model | Two-way data binding | - |
| v-text | Set text content | - |
| v-html | Set raw HTML | - |
| v-once | Render once, skip updates | - |
| v-pre | Skip compilation | - |
<template>
<div>
<!-- v-if / v-else-if / v-else - removes from DOM -->
<div v-if="role === 'admin'">Admin Panel</div>
<div v-else-if="role === 'user'">User Dashboard</div>
<div v-else>Guest View</div>
<!-- v-show - toggles display:none, stays in DOM -->
<div v-show="isVisible">I'm visible: {{ isVisible }}</div>
<!-- Use v-show for frequent toggles, v-if for rare ones -->
<!-- v-bind shorthand : -->
<img :src="imgSrc" :alt="imgAlt" :class="{ rounded: isRound }" />
<a :href="url" :target="newTab ? '_blank' : '_self'">Link</a>
<!-- v-on shorthand @ -->
<button @click="handleClick">Click</button>
<button @click="count++">Count: {{ count }}</button>
<input @keyup.enter="submit" @keyup.esc="cancel" />
<form @submit.prevent="handleSubmit">...</form>
<!-- v-model - two-way binding -->
<input v-model="text" />
<p>You typed: {{ text }}</p>
<!-- v-model modifiers -->
<input v-model.trim="trimmed" /> <!-- trim whitespace -->
<input v-model.number="age" type="number" /> <!-- convert to number -->
<input v-model.lazy="lazy" /> <!-- update on change, not input -->
<!-- v-once - render once, no reactivity -->
<p v-once>Initial value: {{ count }}</p>
<!-- Template with v-if (no extra DOM element) -->
<template v-if="showGroup">
<h3>Group Title</h3>
<p>Group content</p>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
const role = ref('admin')
const isVisible = ref(true)
const imgSrc = ref('/photo.jpg')
const imgAlt = ref('Photo')
const isRound = ref(true)
const url = ref('https://vuejs.org')
const newTab = ref(true)
const count = ref(0)
const text = ref('')
const trimmed = ref('')
const age = ref(0)
const lazy = ref('')
const showGroup = ref(true)
function handleClick() { alert('Clicked!') }
function submit() { console.log('Submitted') }
function cancel() { console.log('Cancelled') }
function handleSubmit() { console.log('Form submitted') }
</script>
<template>
<div>
<!-- v-for with array -->
<ul>
<li v-for="(item, index) in fruits" :key="index">
{{ index + 1 }}. {{ item }}
</li>
</ul>
<!-- v-for with objects -->
<div v-for="user in users" :key="user.id" class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span :class="`badge-${user.role}`">{{ user.role }}</span>
</div>
<!-- v-for with range -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- v-for with object properties -->
<div v-for="(value, key, index) in person" :key="key">
{{ index }}. {{ key }}: {{ value }}
</div>
<!-- v-for + v-if - use template to avoid conflict -->
<template v-for="user in users" :key="user.id">
<div v-if="user.active">{{ user.name }}</div>
</template>
<!-- Dynamic list operations -->
<button @click="addUser">Add User</button>
<button @click="removeUser(0)">Remove First</button>
<button @click="sortUsers">Sort</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const fruits = ref(['Apple', 'Banana', 'Cherry'])
const users = reactive([
{ id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', role: 'user', active: false },
{ id: 3, name: 'Carol', email: 'carol@example.com', role: 'user', active: true },
])
const person = reactive({ name: 'Alice', age: 25, city: 'NYC' })
function addUser() {
users.push({ id: Date.now(), name: 'New User', email: 'new@example.com', role: 'user', active: true })
}
function removeUser(index) { users.splice(index, 1) }
function sortUsers() { users.sort((a, b) => a.name.localeCompare(b.name)) }
</script>
Directive hooks receive the element, binding, vnode, and previous vnode as appropriate. Initialize DOM resources when mounted, respond to relevant binding changes in updated, and disconnect observers or destroy widgets in unmounted. Store only narrowly required cleanup data on the element or in a private WeakMap.
Treat binding values as untrusted configuration and avoid injecting raw HTML. A directive cannot replace a component when the feature owns markup, state, and events. Register locally unless the behavior is genuinely available throughout the application.
v-bind updates an attribute or property, v-on registers an event listener, v-model coordinates value and update events, v-if creates and destroys a branch, and v-show keeps the element while toggling display. Choose from lifecycle and frequency: v-show suits frequent visibility changes, while v-if avoids creating a branch that may never be needed.
v-for needs a stable key when rendered items can move or keep local state. Avoid placing v-if and v-for on the same element because their precedence makes scope and filtering harder to reason about; derive the filtered collection or move the condition to a wrapper.
A directive can receive an argument such as href in v-bind:href and modifiers such as prevent or stop in an event directive. Shorthands preserve the same semantics, so @submit.prevent still combines event registration with default-action prevention. Modifier order matters when generated handlers perform operations in sequence.
Dynamic arguments must resolve to a valid name or null and are harder to statically inspect. Prefer explicit bindings when names are known. Treat raw HTML as a trust boundary: v-html inserts HTML and must never receive unsanitized user-controlled content.
Explore 500+ free tutorials across 20+ languages and frameworks.