So, you're looking to learn Vue.js. That's a fantastic choice. Seriously. I can still remember when I first started exploring modern JavaScript frameworks. It felt... well, a little overwhelming. There was so much boilerplate, a ton of new concepts to wrap my head around, and this general "you should already know this" vibe everywhere I looked.
Then I found Vue. And it just clicked. It felt like it was built by someone who actually remembered the struggle of being a beginner. It was approachable, the documentation was genuinely helpful, and things just... worked.
This guide is my shot at giving you that same "aha!" moment. We're not going to get lost in abstract theory. We're going to jump right in, build something real, and make sure you understand the why behind the code. Consider this your practical, no-fluff Vue.js 3 getting started guide, focusing on the modern, powerful way of doing things: the Composition API.
Ready? Let’s get to it.
What's the Big Deal with Vue, Anyway?
Before we type a single line of code, let's chat for a second. Why Vue?
You'll hear Vue.js call itself "The Progressive JavaScript Framework." That sounds fancy, but what does it actually mean? Honestly, it just means it scales with you. You can start small, maybe just sprinkling a little interactivity onto an existing HTML page. Or, you can go all-in and build a massive, complex single-page application (SPA). It doesn't force you into one way of doing things.
The core idea is beautifully simple: you define your app's state (your data), and you create a template (your HTML) to display it. When your state changes, Vue jumps in and efficiently updates the template to match. That's really the magic of it. No more manually hunting for DOM elements with getElementById and messing with their innerHTML. What a relief.
And with Vue 3, the Composition API came along and made organizing complex logic an absolute dream. We'll be using it right from the start because, frankly, it's the future of Vue and the best way to learn.
Setting Up Your Playground: Two Paths
You've basically got two main ways to get started with Vue. There's the super-quick, "let's just see what this is" way, and then there's the "okay, let's build a real app" way.
Path 1: The "I Have 5 Minutes" CDN Link
This method is perfect for just messing around or adding a bit of Vue to a simple, existing project. All you do is drop a <script> tag into an HTML file, and you're good to go. No build tools, no installations—just pure, simple Vue.
<!DOCTYPE html>
<html>
<head>
<title>My First Vue App</title>
<!-- This is the magic script that gives us Vue -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<!-- This div is our Vue app's territory -->
<div id="app">
<h1>{{ message }}</h1>
<p>You've clicked the button {{ count }} times.</p>
<button @click="count++">Click Me!</button>
</div>
<script>
// Pull what we need from the global `Vue` object
const { createApp, ref } = Vue;
// This is our app's brain
createApp({
setup() {
const message = ref('Hello, World! This is Vue.');
const count = ref(0);
// Anything we return here is available in our HTML
return {
message,
count
};
}
}).mount('#app'); // Tell Vue where to live
</script>
</body>
</html>
Just open that HTML file in your browser, and voilà—you've got a working Vue app. Go ahead, click the button. See the number go up? That's reactivity in action. We'll get to what ref and setup mean in a minute. For now, just take a second to appreciate how simple that was.
Path 2: The "Real Project" Setup with Vite (Recommended)
Okay, the CDN approach is great for a quick spin, but for anything you're actually going to build, you'll want a proper development setup. This gets you awesome features like hot-reloading (your browser updates instantly as you code), code optimization, and the whole single-file component workflow, which is where Vue truly shines.
Thankfully, this is incredibly easy these days with a tool called Vite (pronounced "veet," like the French word for "quick").
Fire up your terminal and run this one command:
npm create vue@latest
This will start up a little interactive questionnaire. Don't worry, it's not a test! Here’s my go-to cheat sheet for beginners:
- Project name?
my-awesome-vue-app(or whatever you'd like) - Add TypeScript? Let's stick with No for now to keep things simple.
- Add JSX Support? No. We're here for Vue's awesome templates.
- Add Vue Router for Single-Page Applications? Let's say No for this first project. You can always add it later.
- Add Pinia for state management? No. We won't need it just yet.
- Add Vitest for Unit Testing? No.
- Add End-to-End Testing Solution? No.
- Add ESLint for code quality? Yes. This is a great habit to get into. It's like a friendly grammar checker for your code.
Once that's finished, just follow the simple instructions it gives you:
cd my-awesome-vue-app
npm install
npm run dev
Your terminal will spit out a local URL, probably something like http://localhost:5173. Open that in your browser. You've just created a modern, professional, ready-for-production Vue application. How cool is that?
The Anatomy of a Vue Component
Now, open that new project in your favorite code editor. Find your way to src/App.vue. This is your very first Single-File Component (SFC), and it's a thing of beauty. It lets you keep the HTML, JavaScript, and CSS for a piece of your user interface all together in one tidy file.
A typical Vue 3 component is split into three parts:
<!-- MyFirstComponent.vue -->
<!-- Part 1: The JavaScript Logic -->
<script setup>
// All our component's logic and state lives here.
// It's like the component's brain.
</script>
<!-- Part 2: The HTML Template -->
<template>
<!-- This is what the component will render. -->
<!-- It's the component's face. -->
</template>
<!-- Part 3: The Styles -->
<style scoped>
/* These styles ONLY apply to this component. */
/* No more accidental style conflicts! */
</style>
That setup attribute in the <script> tag is how we tell Vue, "Hey, we're using the modern Composition API here." And the scoped attribute in the <style> tag is an absolute lifesaver—it means any CSS you write here won't leak out and accidentally mess up other parts of your app. This is a huge win for keeping your projects maintainable.
The Heart of Vue: Reactive State
Alright, let's get to the real magic. How does Vue know it needs to update the page when some data changes? The secret sauce is called reactivity.
In the Composition API, we have two main tools for creating reactive data: ref and reactive.
ref: For Simple Values
The easiest way to think of a ref is like a little box. You put a single value inside it—a number, a string, a boolean—and you give that box a name. Vue then keeps an eye on the box. If the value inside the box ever changes, Vue knows it needs to update any part of the page that's looking at that box.
Let's give it a try. Go ahead and replace the entire content of src/App.vue with this:
<script setup>
import { ref } from 'vue';
// Create a reactive "box" called `message`
// containing the string 'Welcome to Your Vue App'.
const message = ref('Welcome to Your Vue App');
// Another box for a counter
const counter = ref(0);
function increment() {
// To change the value *inside* the box, we use .value
counter.value++;
}
</script>
<template>
<h1>{{ message }}</h1>
<div class="card">
<button @click="increment">
Count is {{ counter }}
</button>
</div>
</template>
<style scoped>
.card {
padding: 2em;
}
</style>
Here's the key takeaway: When you're working in the <script> section, you have to use .value to get to the value inside a ref. But in the <template>, Vue is smart and "unwraps" it for you, so you can just write {{ counter }}. This trips up a lot of newcomers, so just burn this into your brain: script needs .value, template doesn't.
reactive: For Complex Objects
While ref is perfect for simple, primitive values, it can feel a little clunky when you're working with objects. That's where reactive shines. It takes a whole object and makes every single property inside it reactive.
<script setup>
import { reactive } from 'vue';
// Create a single reactive object to hold our form state
const formState = reactive({
username: '',
email: '',
isActive: true,
});
function toggleStatus() {
// No .value needed here! Just mutate the object directly.
formState.isActive = !formState.isActive;
}
</script>
<template>
<form>
<input v-model="formState.username" placeholder="Username" />
<input v-model="formState.email" placeholder="Email" />
</form>
<p>User: {{ formState.username }}</p>
<p>Status: {{ formState.isActive ? 'Active' : 'Inactive' }}</p>
<button @click="toggleStatus">Toggle Status</button>
</template>
See the difference? With reactive, we don't need .value at all. You just access the properties directly on the object. It's so much cleaner for managing groups of related data.
So, when do you use ref vs reactive? Here's my rule of thumb: use ref for primitives (strings, numbers, booleans) and use reactive for objects. You'll get a feel for it as you go. Don't stress about picking the "perfect" one right now.
Computed Properties: Your Smart, Derived Data
Sometimes, you have a piece of data that depends on other data. A classic example is a fullName that's made up of a firstName and a lastName. You could just stitch them together every time you need them, but that's inefficient and messy.
This is the perfect job for a computed property. Think of them as reactive values that are intelligently derived from other reactive values. The best part? They're cached. That means they only re-calculate their value when one of their dependencies changes. It's super efficient!
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// This computed property will automatically update
// whenever firstName or lastName changes.
const fullName = computed(() => {
console.log('Calculating full name...'); // This will only log when a name changes
return `${firstName.value} ${lastName.value}`;
});
</script>
<template>
<input v-model="firstName" placeholder="First Name" />
<input v-model="lastName" placeholder="Last Name" />
<h2>Hello, {{ fullName }}!</h2>
</template>
Try typing in the input fields. The fullName updates instantly, right? But if you check your browser's console, you'll see the console.log only runs when you actually change one of the names. That's the power of caching. Computed properties are honestly one of my favorite features in Vue.
Talking to the DOM: Event Handling
So far, we've seen @click. This is Vue's clean, simple way of listening for DOM events. The @ symbol is just a handy shorthand for v-on:. So, @click is exactly the same as writing v-on:click.
You can use this for any DOM event you can think of: @submit, @mouseover, @keydown.enter... yep, you can even add key modifiers right in the template!
<script setup>
import { ref } from 'vue';
const message = ref('');
function showAlert() {
alert('You submitted the form!');
}
</script>
<template>
<!-- The .prevent modifier is like calling event.preventDefault() -->
<form @submit.prevent="showAlert">
<input
v-model="message"
@keyup.enter="showAlert"
placeholder="Type something and hit enter"
/>
<button type="submit">Submit</button>
</form>
</template>
This is so much more declarative and easier to read than adding event listeners the old-fashioned way in JavaScript. You can tell exactly what the template is supposed to do just by looking at it.
Let's Build Something: A Simple Todo List
Theory is great, but nothing beats getting your hands dirty. Let's pull everything we've just learned together and build the classic "Hello, World!" of web frameworks: a todo list app.
Go ahead and delete everything inside App.vue so we can start with a clean slate.
1. The State
First things first, what data do we actually need to keep track of?
- A list to hold all of our todos.
- A place to store the text for a new todo while the user is typing it.
Let's get that set up in our script.
<script setup>
import { ref, computed } from 'vue';
// A ref to hold the text of the new todo
const newTodoText = ref('');
// A ref to hold our array of todo objects
const todos = ref([
{ id: 1, text: 'Learn the basics of Vue', done: true },
{ id: 2, text: 'Build a simple app', done: false },
{ id: 3, text: 'Fall in love with Vue', done: false },
]);
</script>
2. The Template (Displaying Todos)
Okay, let's get those todos showing up on the screen. We need to loop over our todos array and render an <li> element for each one. For that, we'll use Vue's v-for directive.
<template>
<div class="todo-app">
<h1>My Todo List</h1>
<!-- We'll add the form here later -->
<ul>
<!-- Loop through each `todo` in the `todos` array -->
<!-- The :key is crucial for Vue to track each item efficiently -->
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</div>
</template>
That :key is really important. It's a unique identifier that helps Vue keep track of which list item is which, so it can be super efficient when items are added, removed, or reordered. Just make sure it's a unique value for each item in the loop, like an id from a database.
3. Adding New Todos
Great, we can see our list. Now we need a form so we can add new items. This will involve an <input> field linked with v-model and a function to handle what happens when we submit the form.
Let's add the logic to our <script> first:
// Inside <script setup>
let nextTodoId = 4; // To generate unique IDs
function addTodo() {
// A little validation to prevent adding empty todos
if (newTodoText.value.trim() === '') {
return;
}
// Add the new todo to our array
todos.value.push({
id: nextTodoId++,
text: newTodoText.value,
done: false
});
// And clear the input field for the next one
newTodoText.value = '';
}
Now, let's wire that up in our template:
<template>
<div class="todo-app">
<h1>My Todo List</h1>
<!-- Form to add a new todo -->
<form @submit.prevent="addTodo">
<input
v-model="newTodoText"
placeholder="What needs to be done?"
/>
<button type="submit">Add Todo</button>
</form>
<ul>
<!-- ... our v-for loop ... -->
</ul>
</div>
</template>
Give it a shot! You should be able to add new items to your list. Notice how v-model keeps our newTodoText variable perfectly in sync with the input box, and @submit.prevent calls our addTodo function without that annoying page reload? It's all starting to come together.
4. Toggling and Removing Todos
Let's add the finishing touches. We need a way to mark todos as complete and a way to remove them entirely.
<!-- Update the li in our v-for loop -->
<li v-for="todo in todos" :key="todo.id" :class="{ done: todo.done }">
<input type="checkbox" v-model="todo.done" />
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">X</button>
</li>
We just added three cool things here:
- A dynamic class binding:
:class="{ done: todo.done }". This tells Vue to add the CSS classdoneto the<li>only when that todo'sdoneproperty istrue. - A checkbox with
v-model="todo.done". This is so powerful. It creates a two-way binding directly to thedoneproperty of the specifictodoobject in our array. Checking the box changes the data, and changing the data checks the box. - A remove button that calls a new
removeTodofunction, passing along theidof the todo we want to delete.
Now, we just need to create that removeTodo function in our script:
// Inside <script setup>
function removeTodo(idToRemove) {
// We'll just filter the array, keeping everything EXCEPT the todo with the matching ID.
todos.value = todos.value.filter(todo => todo.id !== idToRemove);
}
And finally, let's throw in a little CSS to make it look less plain.
<style scoped>
.todo-app {
max-width: 500px;
margin: 40px auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.done span {
text-decoration: line-through;
color: #888;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
}
li button {
margin-left: auto;
background: #f44336;
color: white;
border: none;
cursor: pointer;
}
</style>
And there you have it! A complete, fully functional, reactive Todo application. You've just used reactive state, event handling, list rendering, and conditional classes. You've honestly just covered about 70% of what you'll be doing with Vue on a day-to-day basis.
For a little extra practice, you could try adding a computed property that shows a message like "3 items left to complete." I'll leave that as a fun little challenge for you!
You've Only Scratched the Surface
Look, we've covered a ton of ground here, but this is truly just the start of your journey. The real power of Vue comes alive when you start breaking your app down into small, reusable components. We did it with App.vue, but imagine creating a <TodoItem> component that you could reuse and manage independently. That's your next step.
Don't feel like you have to know everything all at once. The most important thing you can do now is to keep building. Think of a small project—a weather app, a recipe book, a movie tracker, anything—and just try to build it. You will get stuck. You will have to look things up in the official Vue docs. And you will learn an incredible amount along the way.
Welcome to the Vue community. We're really happy to have you.
Frequently Asked Questions
What's the deal with
<script setup>? Is it new?Yep, it is! It's the recommended and by far the most common way to use the Composition API today. In some older Vue 3 tutorials, you might see a
setup()function that has areturnstatement at the end.<script setup>is just "syntactic sugar" for that—it's cleaner, more concise, and saves you from writing boilerplate. Just use it. Trust me.
When should I use
refvsreactive?This is a super common question! The simplest rule that will serve you well is: use
reffor primitive values (like a string, number, or boolean) and usereactivefor objects with multiple properties (like a user profile or a form's state). You actually can't put a primitive inreactive, sorefis your go-to for those. As you get more advanced, you'll see some nuances, but this rule of thumb will work for you 95% of the time.
Is the Options API (from Vue 2) dead?
Not at all! It's still 100% supported in Vue 3 and works just fine. However, the Composition API (which we've used here) is what the Vue team recommends for all new applications, especially as they get bigger. It makes organizing and reusing your code so much easier in the long run. If you're starting fresh today, learning the Composition API first is definitely the way to go.