Updated June 2026. Tested on Vue 3 with
<script setup>. Part of the Techalyst Vue 3 series.
Directives are the special v- attributes you sprinkle through a Vue template to wire markup to your data. Most of them you reach for every day without thinking, but each has a sharp edge or two worth knowing. Here is the practical tour, plus how to build your own.
How a directive is put together
Every directive follows the same shape:
v-name:argument.modifier="value"
- The name always starts with
v-. - The argument comes after a colon (for
v-bind:href, the argument ishref). - Modifiers are dot-suffixed flags (
.stop,.prevent,.number). - The value is a JavaScript expression evaluated in the same scope as a
{{ mustache }}, so your reactive state is reachable by name. Notewindowis not in that scope, soconsole.log(...)inside a directive value does nothing.
v-bind takes over the attribute
v-bind (shorthand :) binds an attribute to an expression. The important mental shift: once you prefix an attribute with :, it stops being a plain HTML attribute and becomes an argument of the directive. That changes its behaviour, most visibly on boolean attributes like disabled.
<!-- Plain HTML: any value, even "false", keeps it disabled -->
<input disabled="false" />
<!-- v-bind: false removes the attribute entirely -->
<input :disabled="false" />
<input :disabled="isDisabled" />
So v-bind does not only set a value, it decides whether the whole attribute is rendered at all. Keep the expression simple, one property name is ideal. If you need processing, push it into a computed property.
v-text and v-html
v-text is just the attribute form of a mustache tag, setting the whole text content of the element.
<h2>{{ title }}</h2>
<h2 v-text="title"></h2>
v-html renders a string as real HTML instead of escaping it. It is genuinely dangerous: the content is injected as raw HTML, not compiled as a Vue template, so any user-supplied string is an XSS hole. Only use it on content you fully trust.
<h2 v-html="trustedHtml"></h2>
v-once, v-pre and v-cloak
These three are about rendering control rather than data.
v-oncerenders the element a single time and then freezes it, ignoring later updates. Handy for genuinely static content.v-preskips compilation of the element and its children entirely, so{{ title }}shows up literally. Useful for documenting syntax, or as a micro-optimisation on big static blocks.v-cloakhides an element until Vue has finished compiling, so users never glimpse raw{{ }}tags. It does nothing on its own, you must pair it with CSS:
[v-cloak] { display: none; }
Vue strips the attribute once compilation finishes, revealing the element.
v-show vs v-if
Both hide things, but very differently.
v-showtogglesdisplay: none. The element stays in the DOM, so toggling is cheap. Good for things flipped on and off often.v-ifadds and removes the element from the DOM completely. Heavier to toggle, but nothing renders when the condition is false. It also supportsv-else-ifandv-elsechains, whichv-showdoes not.
<p v-if="fruit === 'apple'">Apple</p>
<p v-else-if="fruit === 'orange'">Orange</p>
<p v-else>Something else</p>
The chain links must sit on immediately adjacent elements. Slip a stray element between a v-if and its v-else and Vue warns that the v-else has no matching v-if.
v-show |
v-if |
|
|---|---|---|
| Hidden element | Stays in DOM (display:none) |
Removed from DOM |
Works with v-else |
No | Yes |
| Best for | Frequent toggling | Rarely shown content |
v-for and the trap
v-for repeats the element it sits on. Always give it a :key.
<script setup>
import { ref } from 'vue'
const records = ref([
{ id: 1, brand: 'Ford', year: 2020 },
{ id: 2, brand: 'Toyota', year: 2019 },
])
</script>
<template>
<ul>
<li v-for="record in records" :key="record.id">
{{ record.brand }} ({{ record.year }})
</li>
</ul>
</template>
Prefer a stable, unique key like a database id over the array index. Index keys break when you insert or remove items in the middle of the list, because Vue reuses the wrong DOM nodes.
You can also loop an object with (value, key, index):
<li v-for="(value, key, index) in nation" :key="key">
{{ key }}: {{ value }}
</li>
The trap: never put v-if and v-for on the same element. In Vue 3 v-if has the higher priority, so it evaluates before the loop alias even exists, which is both confusing and a warning. Put the v-for on the element and the v-if on a child, or filter the list in a computed property first.
<!-- Filter the data, do not fight the template -->
<li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
And since v-for renders in array order, sort the array in a computed property rather than trying to sort inside the template.
Writing a custom directive
When you need to touch the DOM directly across several elements, a custom directive is the right tool. Register one globally on the app, or locally in a component.
// main.js, available everywhere
app.directive('adjust', (el, binding) => {
el.style.fontSize = binding.value + 'px'
el.style.textAlign = binding.arg
})
With <script setup>, any top-level binding named vSomething becomes the local directive v-something:
<script setup>
const vFocus = {
mounted: (el) => el.focus(),
}
</script>
<template>
<input v-focus />
</template>
The function form runs on both mounted and updated, which covers most cases. For finer control, pass an object of lifecycle hooks (created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted). Each hook receives el (the real DOM node) and binding, where binding.value is the expression result, binding.arg is the argument after the colon, and binding.modifiers is the modifier map.
<template>
<h1 v-adjust:[align]="size">Heading</h1>
</template>
One caveat: this inside a directive hook points at window, not your component, so reach the instance through binding.instance if you ever need it.
Wrapping up
v-bind owns the attribute it touches, v-html is an XSS risk so treat it with suspicion, and the v-show versus v-if choice comes down to how often you toggle. Always key your v-for, prefer stable keys, and keep v-if and v-for off the same element. When plain bindings are not enough and you genuinely need DOM access, write a small custom directive.
More in the series: handling events with v-on and class and style bindings. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.