Vue is a progressive JavaScript framework for building user interfaces. You describe what the interface should look like for the current data, and Vue updates the page by itself whenever the data changes. You can add it to an existing website with a single <script> tag or build a large single-page application (SPA) on top of it.

The problem: manual DOM work

Take a counter: the number of clicks, a hint that changes after the fifth click, and a "Reset" button that stays disabled while the counter is zero. In plain JavaScript:

html
<p>Clicks: <span id="count">0</span></p>
<p id="hint">Click the button</p>
<button id="inc">+1</button>
<button id="reset" disabled>Reset</button>

<script>
  let count = 0

  const countEl = document.querySelector('#count')
  const hintEl = document.querySelector('#hint')
  const resetBtn = document.querySelector('#reset')

  // after every data change we manually update every dependent place
  function render() {
    countEl.textContent = count
    hintEl.textContent = count >= 5 ? 'Enough clicking!' : 'Click the button'
    resetBtn.disabled = count === 0
  }

  document.querySelector('#inc').addEventListener('click', () => {
    count++
    render()
  })

  resetBtn.addEventListener('click', () => {
    count = 0
    render()
  })
</script>

The code works, but it is full of bookkeeping: find the elements, attach handlers, call render() after every change and update every dependent place inside it. This is the imperative approach: you tell the browser how to change the page step by step. With hundreds of elements and data changing from many places, it is easy to forget to update one of them — and the interface starts showing wrong information.

Declarative UI: the same counter in Vue

Vue offers a declarative approach: you describe once what should be on the screen depending on the data, and the framework takes care of keeping it in sync. In short: UI = f(state).

html
<div id="app">
  <p>Clicks: {{ count }}</p>
  <p>{{ count >= 5 ? 'Enough clicking!' : 'Click the button' }}</p>
  <button @click="count++">+1</button>
  <button :disabled="count === 0" @click="count = 0">Reset</button>
</div>

<script type="module">
  import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

  createApp({
    setup() {
      const count = ref(0) // a reactive value: Vue tracks its changes
      return { count }     // make count available in the template
    }
  }).mount('#app')         // Vue controls the content of <div id="app">
</script>

The markup inside #app has become a template: {{ count }} outputs a value, @click attaches a handler, :disabled binds an attribute to an expression (covered in detail in the templates section). ref(0) creates a reactive value: when count changes, Vue updates the dependent places itself. After five clicks the DOM looks like this:

html
<div id="app" data-v-app="">
  <p>Clicks: 5</p>
  <p>Enough clicking!</p>
  <button>+1</button>
  <button>Reset</button>
</div>

The button lost disabled and the hint changed — without any extra code (data-v-app marks the app container).

💡 Save the example as index.html and open it in a browser — no installation needed. Online sandbox: Vue Playground.

A "progressive" framework

"Progressive" means you adopt Vue gradually, as much as the project needs:

LevelHow it is addedWhen it fits
Widget on a page<script type="module"> from a CDNa calculator or filter on an existing site (Laravel, WordPress)
SPAVite project, .vue filesuser dashboard, admin panel
SPA + ecosystem+ Vue Router, Piniamany pages and shared state
Nuxtmeta-framework on top of VueSEO, server-side rendering

The first level: Vue "brings to life" a single block on a regular server-generated page.

html
<main>
  <h1>Delivery</h1>
  <!-- regular page HTML: Vue does not touch it -->
  <p>Free delivery for orders over 50 AZN.</p>

  <div id="delivery-calc">
    <label>Weight: <input v-model.number="weight" type="number" min="0"> kg</label>
    <p>Price: {{ price }} AZN</p>
  </div>
</main>

<script type="importmap">
  { "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js" } }
</script>

<script type="module">
  import { createApp, ref, computed } from 'vue'

  createApp({
    setup() {
      const weight = ref(1)
      const price = computed(() => 5 + weight.value * 2) // 5 AZN + 2 AZN per kg
      return { weight, price }
    }
  }).mount('#delivery-calc')
</script>

Vue controls only #delivery-calc: type a weight of 3 and the price becomes 11 AZN right away. An import map lets you write from 'vue' instead of the full URL, and .prod.js is the production build. We will study v-model and computed later.

In an SPA the code moves into single-file components — .vue files where the template, logic and styles live together:

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <p>Clicks: {{ count }}</p>
  <button @click="count++">+1</button>
  <button :disabled="count === 0" @click="count = 0">Reset</button>
</template>

The template is the same, while <script setup> removes the need for createApp and return: everything declared in the script is available in the template. .vue files are compiled by Vite — this is how we will write code for the rest of the course.

Vue among other frameworks

All four build declarative UIs out of components, but with different philosophies:

VueReactAngularSvelte
What it isprogressive frameworkUI library"batteries included" platformcompiler framework
MarkupHTML templates (JSX optional)JSXHTML templatesHTML templates
ReactivityProxy-based ref/reactiveuseState, component re-renderSignalsrunes: $state
Routing and stateofficial Vue Router and Piniathird-party librariesbuilt inSvelteKit
Meta-frameworkNuxtNext.jsAngular SSRSvelteKit

Vue's strengths are templates in familiar HTML, fine-grained reactivity, excellent documentation and official tooling. It is popular in the Laravel ecosystem, and GitLab's interface is built with it. The knowledge transfers: components and reactivity work in similar ways in all four frameworks.

Ecosystem and current version

ToolPurposeWhere in the course
Vitedev server and buildnext topic
Vue Routerpages and navigation in an SPA"Routing" section
Piniashared state store"Pinia" section
VueUsehundreds of ready-made composables"Composables" section
NuxtSSR, SEO, file-based routesfinal section

The current line is Vue 3.5 (3.5.43 at the time of writing); version 3.6 with the Vapor Mode rendering strategy is in Release Candidate status. Vue 2 has been unsupported since December 31, 2023, but there are still plenty of examples for it online.

Common mistakes

Changing the DOM by hand inside a Vue app. document.querySelector('#count').textContent = 10 only lasts until the next update — Vue re-renders the element from its own data. Change the data (count.value = 10), not the DOM.

Forgetting type="module". A classic script does not understand import, and the browser throws Uncaught SyntaxError: Cannot use import statement outside a module.

html
<script>import { createApp } from 'vue'</script>               <!-- ❌ -->
<script type="module">import { createApp } from 'vue'</script> <!-- ✅ -->

Copying Vue 2 examples. new Vue({ el, data }), Vue.component(), {{ price | currency }} filters and $on do not work in Vue 3. Look for createApp in examples.

Leaving the dev build on a live site. vue.esm-browser.js is heavier and logs "You are running a development build of Vue" to the console. Production needs vue.esm-browser.prod.js, and a full application needs a Vite project.

Summary

  • Vue is a framework for declarative UI: you describe the result based on data, and Vue updates the DOM.
  • ref() creates a reactive value, a template with {{ }}, @click and :disabled describes the markup, and createApp().mount() starts the app.
  • "Progressive" means adoptable step by step: from a CDN widget to an SPA on Vite and Nuxt.
  • Vue offers HTML templates, fine-grained reactivity and an official ecosystem: Vite, Vue Router, Pinia, VueUse, Nuxt.
  • The current version is Vue 3.5; code with new Vue() belongs to the outdated Vue 2.

Practice

  1. Open Vue Playground, change the value the example displays and watch the result on the right update.
  2. Save the Vue counter as index.html and open it in a browser. Add a "-1" button that is disabled at zero.
  3. Add an "Even"/"Odd" line to both versions of the counter (JS and Vue) — whether the number of clicks is even or odd. Compare how many lines you had to change in each.
  4. Build a "Tip calculator" widget for a café page: a bill amount field, a percentage field and a line "Tip: … AZN, Total: … AZN". Vue controls only the #tip-calc block.

💡 In task 4 use an import map and computed, as in the delivery example, plus v-model.number — otherwise the field values arrive as strings.