Skip to main content

⚙️ 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 ref and reactive in <script setup>
  • Bind text, attributes, and events with {{ }}, v-bind, and v-on
  • Render conditionally and iterate with v-if, v-show, and v-for
  • Build two-way form bindings with v-model and 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.
flowchart LR A[Template with v-directives] --> B[Vue Compiler] B --> C[Render Function] C --> D[Virtual DOM] D --> E[Real DOM] F[Reactive State] -.->|drives| A

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:

  1. A text input bound with v-model where the user types their name.
  2. A <select> (also v-model) to pick a background color.
  3. A live badge preview whose background uses :style and whose text shows the name.
  4. A button that toggles the badge's visibility with v-show (or v-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) and reactive (object state).
  • Directives connect state to the DOM: v-bind for attributes, v-on for events, v-if/v-show for conditions, v-for for lists.
  • v-model gives you two-way form binding for free.
  • Custom directives package reusable DOM behavior; always give v-for a 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.