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:
<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).
<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:
<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.htmland 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:
| Level | How it is added | When it fits |
|---|---|---|
| Widget on a page | <script type="module"> from a CDN | a calculator or filter on an existing site (Laravel, WordPress) |
| SPA | Vite project, .vue files | user dashboard, admin panel |
| SPA + ecosystem | + Vue Router, Pinia | many pages and shared state |
| Nuxt | meta-framework on top of Vue | SEO, server-side rendering |
The first level: Vue "brings to life" a single block on a regular server-generated page.
<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:
<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:
| Vue | React | Angular | Svelte | |
|---|---|---|---|---|
| What it is | progressive framework | UI library | "batteries included" platform | compiler framework |
| Markup | HTML templates (JSX optional) | JSX | HTML templates | HTML templates |
| Reactivity | Proxy-based ref/reactive | useState, component re-render | Signals | runes: $state |
| Routing and state | official Vue Router and Pinia | third-party libraries | built in | SvelteKit |
| Meta-framework | Nuxt | Next.js | Angular SSR | SvelteKit |
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
| Tool | Purpose | Where in the course |
|---|---|---|
| Vite | dev server and build | next topic |
| Vue Router | pages and navigation in an SPA | "Routing" section |
| Pinia | shared state store | "Pinia" section |
| VueUse | hundreds of ready-made composables | "Composables" section |
| Nuxt | SSR, SEO, file-based routes | final 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.
<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{{ }},@clickand:disableddescribes the markup, andcreateApp().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
- Open Vue Playground, change the value the example displays and watch the result on the right update.
- Save the Vue counter as
index.htmland open it in a browser. Add a "-1" button that is disabled at zero. - 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.
- 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-calcblock.
💡 In task 4 use an import map and
computed, as in the delivery example, plusv-model.number— otherwise the field values arrive as strings.