Vue 3 directives: a complete guide to understanding them properly
Vue directives are special attributes that let you apply reactive logic directly to the DOM.
They all start with v- and exist to reduce imperative code and make templates more expressive and declarative.
This post is a high-level map: it doesn’t go extremely deep, but it makes it clear what each directive does, when to use it, and what problem it solves. Each one will later have its own dedicated article.
Series map
If you want to go deeper into each directive, here is the full path for this series:
- Vue Directives: v-if, v-else, and v-show
- Vue Directives: v-for
- Vue Directives: v-bind
- Vue Directives: v-model
- Vue Directives: v-on
- Vue Directives: v-text and v-html
- Vue Directives: v-slot
- Vue Directives: v-once / v-memo / v-pre
- Vue Directives: v-cloak
- Vue Directives: Custom Directives
v-if, v-else-if, v-else
They’re used to render or remove elements from the DOM based on a reactive condition. If the condition is false, the element doesn’t exist in the DOM.
Use it when:
- The content is heavy
- It shouldn’t always exist
- It depends on permissions or critical states
<script setup>
import { ref } from 'vue'
const isLogged = ref(true)
</script>
<template>
<p v-if="isLogged">Welcome</p>
<p v-else>You’re not logged in</p>
</template><script>
export default {
data() {
return {
isLogged: true
}
}
}
</script>
<template>
<p v-if="isLogged">Welcome</p>
<p v-else>You’re not logged in</p>
</template>v-show
It controls visibility using CSS (display: none), but the element always exists in the DOM.
Use it when:
- The element is shown and hidden frequently
- You don’t want to pay the cost of mounting and unmounting the node
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
<template>
<p v-show="isVisible">Visible content</p>
</template><script>
export default {
data() {
return {
isVisible: true
}
}
}
</script>
<template>
<p v-show="isVisible">Visible content</p>
</template>v-for
It lets you render lists from reactive arrays or objects.
Mental key:
v-fordescribes structure, not logic.
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Vue' },
{ id: 2, name: 'React' }
])
</script>
<template>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</template><script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Vue' },
{ id: 2, name: 'React' }
]
}
}
}
</script>
<template>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</template>key is not optional. It never was.
It’s essential so Vue can properly optimize rendering.
v-bind
It dynamically binds HTML attributes or component props.
Think of v-bind like:
“This attribute depends on state”
<script setup>
import { ref } from 'vue'
const imageUrl = ref('https://example.com/image.jpg')
</script>
<template>
<img v-bind:src="imageUrl" alt="Dynamic image" />
</template><script>
export default {
data() {
return {
imageUrl: '/logo.png',
description: 'Logo'
}
}
}
</script>
<template>
<img :src="imageUrl" :alt="description" />
</template>v-model
It creates two-way synchronization between state and an input or component.
It’s ideal for:
- Forms
- Controlled inputs
- Reusable components
<script setup>
import { ref } from 'vue'
const username = ref('')
</script>
<template>
<input v-model="username" placeholder="Enter your username" />
<p>Hello, {{ username }}!</p>
</template><script>
export default {
data() {
return {
username: ''
}
}
}
</script>
<template>
<input v-model="username" placeholder="Enter your username" />
<p>Hello, {{ username }}!</p>
</template>Internally, it combines props and events (modelValue + update:modelValue).
It’s not magic, but it definitely feels like it.
v-on
It listens to DOM events and runs reactive logic.
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<template>
<button v-on:click="increment">You’ve clicked {{ count }} times</button>
</template><script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<template>
<button v-on:click="increment">Click me</button>
<p>You’ve clicked {{ count }} times.</p>
</template>It supports modifiers (.stop, .prevent, .once, etc.) that avoid unnecessary code.
v-text
It inserts plain text into an element, replacing its content.
<script setup>
import { ref } from 'vue'
const message = ref('Hello World')
</script>
<template>
<div v-text="message"></div>
</template><script>
export default {
data() {
return {
message: 'Hello Vue'
}
}
}
</script>
<template>
<div v-text="message"></div>
</template>It’s not used much, but it exists for very specific cases where you don’t want interpolations.
v-html
It inserts unescaped HTML.
<script setup>
import { ref } from 'vue'
const rawHtml = ref('<strong>Bold text</strong>')
</script>
<template>
<div v-html="rawHtml"></div>
</template><script>
export default {
data() {
return {
rawHtml: '<strong>Dynamic HTML</strong>'
}
}
}
</script>
<template>
<div v-html="rawHtml"></div>
</template>⚠️ Never use it with untrusted content. It’s a direct door to XSS if you don’t know exactly what you’re rendering.
v-slot
It lets you define dynamic content inside components via slots.
<script setup>
</script>
<template>
<MyCard>
<template v-slot:header>
<h1>Custom Header</h1>
</template>
<p>Card body content.</p>
<template v-slot:footer>
<button>Action</button>
</template>
</MyCard>
</template><script>
export default {
components: { MyCard }
}
</script>
<template>
<MyCard>
<template v-slot:header>
<h1>Custom Header</h1>
</template>
<p>Card body content.</p>
<template v-slot:footer>
<button>Action</button>
</template>
</MyCard>
</template>It’s key for building flexible, composable, reusable components.
v-once
It renders content only once and excludes it from the reactive system.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<p v-once>This text won’t change: {{ count }}</p>
<button @click="count++">Increment</button>
</template><script>
export default {
data() {
return {
count: 0
}
}
}
</script>
<template>
<p v-once>This text won’t change: {{ count }}</p>
<button @click="count++">Increment</button>
</template>Useful when content should never update, even if state changes.
v-memo
It prevents unnecessary re-renders when dependencies don’t change.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div v-memo="[count]">
<p>This block only re-renders if 'count' changes: {{ count }}</p>
</div>
<button @click="count++">Increment</button>
</template><script>
export default {
data() {
return {
value: 10
}
}
}
</script>
<template>
<div v-memo="[value]">
<p>The value is: {{ value }}</p>
</div>
</template>It’s an advanced optimization. It’s not meant to be used “just because”, but in real bottlenecks.
v-pre
It prevents Vue from compiling the node’s content.
<script setup>
</script>
<template>
<div v-pre>
{{ this_will_not_be_evaluated }}
</div>
</template><script>
export default {}
</script>
<template>
<div v-pre>
{{ this_will_not_be_evaluated }}
</div>
</template>Perfect for showing snippets, literal examples, or demo templates.
v-cloak
It hides the template until Vue finishes mounting the app.
<script setup>
</script>
<template>
<div v-cloak>
{{ message }}
</div>
</template><script>
export default {}
</script>
<template>
<div v-cloak>
{{ message }}
</div>
</template>It prevents the initial flash in client-rendered apps.
Custom directives
They let you extend Vue to directly manipulate the DOM when there’s no more declarative option.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})<template>
<input v-focus />
</template>They’re powerful, but they must be used carefully: if you abuse them, you’re probably breaking Vue’s mental model.
Conclusion
Directives aren’t just fancy syntax. They’re clear contracts between state and the DOM.
This article is the starting point. Each directive will have its own individual post, with:
- Real-world cases
- Common mistakes
- Good and bad practices
This map already helps you read Vue code with good judgment. The rest is depth, not confusion.
