Chapter 70: Vue Directives
Vue Directives, one of the most iconic, most loved, and most instantly recognizable features that makes Vue feel like “HTML on steroids”.
Vue Directives are special attributes that start with v- (like v-if, v-for, v-model, v-bind, v-on, etc.). They are instructions that tell Vue to do something special to the element or component they are attached to.
Think of them like super-powers you attach directly to normal HTML tags:
- Normal HTML: <div>Hello</div>
- Vue with directive: <div v-if=”isLoggedIn”>Welcome back!</div>
Vue sees v-if, understands the logic, and automatically adds/removes the <div> from the DOM based on whether isLoggedIn is true or false.
1. Why Directives Feel Magical (The Big Picture)
Directives are what make Vue declarative instead of imperative.
Imperative (old-school jQuery way):
|
0 1 2 3 4 5 6 7 8 9 10 |
if (user.isLoggedIn) { document.getElementById('welcome').style.display = 'block'; } else { document.getElementById('welcome').style.display = 'none'; } |
Declarative (Vue directive way):
|
0 1 2 3 4 5 6 |
<div id="welcome" v-if="user.isLoggedIn">Welcome back!</div> |
Vue automatically handles showing/hiding — you just describe what should happen, not how to do it.
2. The Core Built-in Directives (What You Use Every Day)
Here’s the main list every Vue developer should know by heart (in rough order of daily usage):
| Directive | Shorthand | What it does | Most common real-world use-case | Example (2026 style) |
|---|---|---|---|---|
| v-bind | : | Bind dynamic value to attribute / prop | class, style, src, id, :to (router), :disabled | :class=”{ active: isActive }” |
| v-on | @ | Listen to events | @click, @submit, @keyup.enter, custom events | @click=”increment” |
| v-model | — | Two-way data binding on form inputs | inputs, textarea, checkbox, select, custom inputs | v-model=”form.email” |
| v-if / v-else-if / v-else | — | Conditional rendering (remove/add from DOM) | show/hide modals, loading states, auth gates | v-if=”user.isLoggedIn” |
| v-show | — | Conditional visibility (display: none) | frequent toggles (tabs, accordions) | v-show=”isLoading” |
| v-for | — | Loop over arrays / objects | lists, tables, cards, options in select | v-for=”todo in todos” :key=”todo.id” |
| v-text | — | Set textContent (safer than {{ }} in some cases) | When you want to avoid mustache parsing issues | v-text=”rawText” |
| v-html | — | Render raw HTML (dangerous – XSS risk!) | Only trusted content from server | v-html=”trustedHtml” |
| v-once | — | Render once, never update again | Static content (prices, timestamps) | <span v-once>{{ price }}</span> |
| v-pre | — | Skip compilation — show as raw text | Debugging or showing mustache syntax literally | <span v-pre>{{ notCompiled }}</span> |
| v-cloak | — | Hide element until Vue finishes compiling | Prevent flash of unstyled content (FOUC) | <div v-cloak>…</div> + CSS [v-cloak] { display: none } |
3. Real, Practical Example – Login Form (using almost all core directives)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
<template> <form @submit.prevent="handleLogin" v-cloak class="login-form"> <!-- v-model + modifiers --> <input v-model.trim.lazy="form.email" type="email" placeholder="Email" required /> <!-- v-if + v-else --> <small v-if="form.email && !isValidEmail" class="error"> Invalid email format </small> <small v-else-if="form.email" class="success"> Looks good! </small> <!-- v-show for loading spinner --> <div v-show="loading" class="spinner">Loading...</div> <!-- v-for + :key (mandatory!) --> <div class="remember-options"> <label v-for="option in rememberChoices" :key="option.value"> <input type="radio" :value="option.value" v-model="form.remember" /> {{ option.label }} </label> </div> <!-- v-bind shorthand (:) for dynamic class & disabled --> <button type="submit" :class="{ 'btn-primary': formIsValid, 'btn-disabled': !formIsValid }" :disabled="!formIsValid || loading" > {{ loading ? 'Logging in…' : 'Login' }} </button> <!-- v-once – static text that never changes --> <p v-once>© 2026 Webliance – All rights reserved</p> </form> </template> <script setup lang="ts"> import { reactive, computed } from 'vue' const form = reactive({ email: '', remember: '30days' }) const rememberChoices = [ { value: 'session', label: 'This session only' }, { value: '30days', label: '30 days' }, { value: 'forever', label: 'Remember forever' } ] const isValidEmail = computed(() => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email) }) const formIsValid = computed(() => form.email && isValidEmail.value) const loading = ref(false) async function handleLogin() { loading.value = true // fake API delay await new Promise(r => setTimeout(r, 1500)) loading.value = false alert('Logged in!') } </script> <style scoped> /* Hide uncompiled mustache until Vue boots */ [v-cloak] { display: none; } .login-form { max-width: 400px; margin: 3rem auto; padding: 2rem; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .error { color: #dc2626; font-size: 0.9rem; margin-top: 0.3rem; } .success { color: #16a34a; font-size: 0.9rem; margin-top: 0.3rem; } .btn-primary { background: #3b82f6; color: white; } .btn-disabled { background: #9ca3af; cursor: not-allowed; } .remember-options { margin: 1rem 0; display: flex; flex-direction: column; gap: 0.5rem; } .spinner { text-align: center; color: #6b7280; margin: 1rem 0; } </style> |
Quick Summary Table – Core Directives You Use Every Day
| Directive | Shorthand | Purpose | Most common gotcha / tip |
|---|---|---|---|
| v-bind | : | Dynamic attributes / props | Always use : for JS values |
| v-on | @ | Event listeners | Modifiers: .prevent, .stop, .once |
| v-model | — | Two-way binding | .trim, .number, .lazy |
| v-if | — | Conditional render (remove/add) | Use <template v-if> for groups |
| v-show | — | Conditional visibility (display: none) | Cheaper than v-if for frequent toggles |
| v-for | — | Loop | Always :key – unique & stable |
| v-html | — | Raw HTML (dangerous!) | Only trusted content – XSS risk |
| v-once | — | Render once, never update | Good for static content |
| v-cloak | — | Hide until Vue boots | Prevent flash of mustache |
Pro Tips from Real Projects (Hyderabad 2026)
- Always use :key in v-for — Vue warns in console if missing
- Prefer shorthand (:class, @click) — cleaner & faster to type
- Use <template> with v-if / v-for when you don’t want extra wrapper <div>
- Never put complex logic inside {{ }} — move to computed
- For accessibility → prefer semantic HTML + proper aria-* attributes with :aria-*
- Use v-cloak + CSS [v-cloak] { display: none } in production to prevent FOUC
Your mini practice:
- Build a small login form using:
- v-model.trim + .lazy
- v-if / v-else for validation messages
- :disabled on submit button
- @submit.prevent
- v-once for copyright footer
- Add v-cloak to the form → see it hidden until Vue boots
Any directive still confusing? Want full examples for:
- v-for + <TransitionGroup> animation?
- Custom component with multiple v-model?
- v-html vs v-text vs {{ }} security comparison?
- All directives in one realistic dashboard card?
Just tell me — we’ll build the next clean, real-world template together 🚀
