⚙️ Vue Instance and Directives
Every Vue app starts the same way: you create an application, give it some reactive state, and mount it to the page. Then directives — those v- attributes — connect that state to the DOM. This lesson turns the concepts from the overview into code you can run.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Create and mount a Vue 3 application with
createApp() - Declare reactive state with
refandreactivein<script setup> - Bind text, attributes, and events with
{{ }},v-bind, andv-on - Render conditionally and iterate with
v-if,v-show, andv-for - Build two-way form bindings with
v-modeland write a custom directive
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Build a reactive task manager using every core directive together.
In This Lesson
Creating the Application
In Vue 3 an app is created with the createApp() function and connected to a DOM element with mount(). Everything inside that element becomes Vue's territory — reactive, managed, and re-rendered on demand.
import { createApp } from 'vue'
import App from './App.vue'
// Create the application from a root component…
const app = createApp(App)
// …and mount it to the <div id="app"> in index.html
app.mount('#app')
🧠 Analogy: The application instance is the brain of your app. Just as the brain coordinates every system in the body, the Vue app orchestrates components, reactivity, and directives across the tree it controls.
⚠️ You'll see older syntax in the wild
Vue 2 code used new Vue({ ... }).$mount('#app'). Vue 3 replaced that global constructor with createApp(), giving each app its own isolated configuration. Recognize the old form when you read legacy projects, but always write the new one.
Reactive State: ref & reactive
With the Composition API you declare state directly in <script setup>. Two functions do almost all the work.
ref — for a single value
Wrap any value (number, string, boolean, or even an object) in ref(). Read and write it through .value in JavaScript; the template unwraps it automatically.
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello Vue!')
count.value++ // now 1
message.value = 'Hi!' // updates everywhere it's used
reactive — for an object of related state
When several values belong together, reactive() makes a whole object reactive. No .value needed — you access properties directly.
import { reactive } from 'vue'
const user = reactive({
name: 'Ada',
loggedIn: false
})
user.loggedIn = true // reactive; the UI updates
Derived and observed state
Two more helpers round out the toolkit:
import { ref, computed, watch } from 'vue'
const count = ref(0)
// computed: cached, recalculated only when `count` changes
const doubled = computed(() => count.value * 2)
// watch: run a side effect when `count` changes
watch(count, (newVal, oldVal) => {
console.log(`count went from ${oldVal} to ${newVal}`)
})
📖 Rule of thumb
Use ref for standalone values and reactive for grouped object state. Use computed to derive values, and watch to trigger side effects (like an API call) when something changes.
What Directives Are
Directives are special template attributes, all prefixed with v-, that apply reactive behavior to the DOM. They're the bridge between your reactive state and what the browser actually renders.
🚦 Analogy: A plain HTML attribute is a static sign — a painted "EXIT" that never changes. A directive is a digital sign that updates its message the moment the underlying data changes.
The directives below cover the vast majority of everyday Vue work. Learn these six and you can build real interfaces.
Binding Text, Attributes & Events
Text interpolation
The double-curly "mustache" syntax inserts a reactive value as text. It's safe by default — HTML is escaped.
<p>{{ message }}</p>
v-bind — dynamic attributes
Mustaches can't go inside attributes, so v-bind (shorthand :) binds an attribute to an expression.
<!-- Full syntax -->
<img v-bind:src="imageUrl" v-bind:alt="imageDescription">
<!-- Shorthand (preferred) -->
<img :src="imageUrl" :alt="imageDescription">
<!-- Conditional classes -->
<div class="btn" :class="{ 'btn-primary': isPrimary, active: isActive }"></div>
v-on — event handling
v-on (shorthand @) listens for DOM events and runs a handler. Handy event modifiers take care of common chores.
<!-- Full and shorthand -->
<button v-on:click="increment">Increment</button>
<button @click="increment">Increment</button>
<!-- Inline expression -->
<button @click="count++">Increment</button>
<!-- Modifiers -->
<form @submit.prevent="submitForm">...</form> <!-- preventDefault -->
<button @click.stop="handleClick">Click</button> <!-- stopPropagation -->
<input @keyup.enter="submit"> <!-- only on Enter -->
⚠️ v-html and XSS
The v-html directive renders raw HTML instead of escaped text. Never use it with user-supplied content — it opens the door to cross-site scripting. Reserve it for trusted, server-sanitized markup only.
Conditional & List Rendering
v-if / v-else-if / v-else
These add or remove elements from the DOM based on a condition.
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else>Neither A nor B</div>
v-show
Similar in effect, but v-show only toggles the CSS display property — the element always stays in the DOM.
<div v-show="isVisible">Toggled with CSS display</div>
💡 v-if vs v-show
Use v-if when a condition rarely flips, or when the content shouldn't exist until needed (it's not even rendered). Use v-show when you toggle something frequently — flipping CSS is cheaper than creating and destroying elements.
v-for — rendering lists
Iterate arrays or objects. Always provide a unique :key so Vue can track each item's identity across updates.
<!-- Array with index -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
<!-- Object properties -->
<ul>
<li v-for="(value, key) in profile" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
⚠️ Never skip :key
Without a stable key, Vue reuses DOM nodes in place and can attach the wrong state (like a checkbox's checked value) to the wrong item after the list reorders. Use a real unique id, not the array index, whenever items can be added, removed, or sorted.
Two-Way Binding with v-model
v-model keeps a form input and a piece of state in sync in both directions. It's shorthand for a v-bind plus a v-on working together.
<!-- Text input -->
<input v-model="message">
<p>Message: {{ message }}</p>
<!-- Checkbox (boolean) -->
<input type="checkbox" v-model="agreed">
<!-- Select dropdown -->
<select v-model="selected">
<option value="A">Option A</option>
<option value="B">Option B</option>
</select>
Under the hood, this:
<input v-model="message">
<!-- is roughly equivalent to -->
<input :value="message" @input="message = $event.target.value">
Useful modifiers
<input v-model.trim="message"> <!-- strip surrounding whitespace -->
<input v-model.number="age"> <!-- cast to a number -->
<input v-model.lazy="message"> <!-- sync on change, not every keystroke -->
Custom Directives
When you need reusable low-level DOM behavior that the built-ins don't cover, write a custom directive. A classic example is auto-focusing an input when it appears.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// Register a global v-focus directive
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
<!-- The input focuses itself as soon as it mounts -->
<input v-focus>
In <script setup> you can also define a local directive by naming a variable with the v prefix:
<script setup>
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus>
</template>
Directive hooks mirror the component lifecycle: beforeMount, mounted, beforeUpdate, updated, beforeUnmount, and unmounted. Real-world uses include tooltips, lazy-loading images, and click-outside handlers.
Worked Example: Task Manager
Let's combine everything — reactive state, computed values, events, conditional and list rendering, two-way binding, and a custom directive — into one small but complete Single-File Component.
<script setup>
import { ref, computed } from 'vue'
const title = ref('Task Manager')
const newTask = ref('')
const tasks = ref([
{ id: 1, text: 'Learn Vue basics', done: false },
{ id: 2, text: 'Build a todo app', done: false }
])
let nextId = 3
// Local custom directive: focus on mount
const vFocus = { mounted: (el) => el.focus() }
const remaining = computed(() => tasks.value.filter(t => !t.done).length)
function addTask() {
const text = newTask.value.trim()
if (!text) return
tasks.value.push({ id: nextId++, text, done: false })
newTask.value = ''
}
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<template>
<h1>{{ title }}</h1>
<!-- v-model + event modifier + attribute binding -->
<input
v-model.trim="newTask"
v-focus
placeholder="Add a task"
@keyup.enter="addTask">
<button @click="addTask" :disabled="!newTask">Add</button>
<!-- Conditional rendering -->
<p v-if="tasks.length === 0">No tasks yet. Add one!</p>
<!-- List rendering with dynamic class -->
<ul v-else>
<li
v-for="task in tasks"
:key="task.id"
:class="{ done: task.done }">
<input type="checkbox" v-model="task.done">
{{ task.text }}
<button @click="removeTask(task.id)">Delete</button>
</li>
</ul>
<p>Remaining: {{ remaining }}</p>
</template>
<style scoped>
.done { text-decoration: line-through; color: gray; }
</style>
What you get
A working task list: type and press Enter (or click Add) to append tasks, tick a checkbox to strike one through, click Delete to remove it, and watch the "Remaining" count update — all with zero manual DOM code.
Hands-on Exercise
🏋️ Build an Interactive Name Badge
Objective: Practice ref, v-model, v-bind styling, and conditional rendering in a single component.
Requirements:
- A text input bound with
v-modelwhere the user types their name. - A
<select>(alsov-model) to pick a background color. - A live badge preview whose background uses
:styleand whose text shows the name. - A button that toggles the badge's visibility with
v-show(orv-if).
💡 Hint
Bind the color like :style="{ backgroundColor: color }". Keep three refs: name, color, and visible. Toggle the last with @click="visible = !visible".
✅ Sample solution
<script setup>
import { ref } from 'vue'
const name = ref('Your Name')
const color = ref('#42b883')
const visible = ref(true)
</script>
<template>
<input v-model="name" placeholder="Enter your name">
<select v-model="color">
<option value="#42b883">Green</option>
<option value="#3b82f6">Blue</option>
<option value="#e44d26">Orange</option>
</select>
<button @click="visible = !visible">Toggle badge</button>
<div
v-show="visible"
class="badge"
:style="{ backgroundColor: color }">
{{ name }}
</div>
</template>
<style scoped>
.badge {
margin-top: 1rem;
padding: 1rem 2rem;
color: white;
border-radius: 8px;
display: inline-block;
font-weight: 700;
}
</style>
🎯 Quick Quiz
Question 1: Which function creates and mounts a Vue 3 application?
Question 2: You need a two-way binding between a text input and a reactive value. Which directive do you use?
Question 3: Why should every v-for include a unique :key?
Summary & Quiz
🎉 Key Takeaways
- Create apps with
createApp(App).mount('#app')in Vue 3. - Declare state with
ref(single values, via.value) andreactive(object state). - Directives connect state to the DOM:
v-bindfor attributes,v-onfor events,v-if/v-showfor conditions,v-forfor lists. v-modelgives you two-way form binding for free.- Custom directives package reusable DOM behavior; always give
v-fora stable:key.
📚 Further Reading
🚀 What's Next?
You can now wire state to the DOM. Next we go deeper into template syntax and reactivity — interpolation nuances, dynamic class and style binding, computed properties, and watchers — so your components stay clean as they grow.
🎉 Great progress!
You just built a real interactive app with nothing but reactive state and directives.