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.
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>If you want to learn more, read the guide Vue Directives: v-if, v-else, and v-show.
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>If you want to learn more, read the guide Vue Directives: v-if, v-else, and v-show.
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.
If you want to learn more, read the guide Vue Directives: v-for.
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>If you want to learn more, read the guide Vue Directive: v-bind.
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.
If you want to learn more, read the guide Vue Directives: v-model.
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.
If you want to learn more, read the guide Vue Directives: v-on.
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.
If you want to learn more, read the guide Vue Directives: v-text and v-html.
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.
If you want to learn more, read the guide Vue Directives: v-text and v-html
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.
If you want to learn more, read the guide Vue Directives: v-slot.
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.
If you want to learn more, read the guide Vue Directives: v-once / v-memo / v-pre.
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.
If you want to learn more, read the guide Vue Directives: v-once / v-memo / v-pre.
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.
If you want to learn more, read the guide Vue Directives: v-once / v-memo / v-pre.
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.
If you want to learn more, read the guide Vue Directives: v-cloak.
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.
If you want to learn more, read the guide Vue Directives: Custom Directives.
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.
