---
title: "Your first Vue component, line by line · vuejs.school"
description: "Create a Vue project with one command, read App.vue top to bottom, and build a counter that updates the page. Every line explained."
canonical_url: "https://vuejs.school/community/blog/your-first-vue-component"
meta:
  "og:description": "Create a Vue project with one command, read App.vue top to bottom, and build a counter that updates the page. Every line explained."
  "og:image": "https://vuejs.school/_og/d/a_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmU%2Cdescription_~Q3JlYXRlIGEgVnVlIHByb2plY3Qgd2l0aCBvbmUgY29tbWFuZCwgcmVhZCBBcHAudnVlIHRvcCB0byBib3R0b20sIGFuZCBidWlsZCBhIGNvdW50ZXIgdGhhdCB1cGRhdGVzIHRoZSBwYWdlLiBFdmVyeSBsaW5lIGV4cGxhaW5lZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy95b3VyLWZpcnN0LXZ1ZS1jb21wb25lbnQi%2Cs_czLkrC9pFRhsGVBi.png"
  "og:image:alt": "Your first Vue component, line by line · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Your first Vue component, line by line · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmU%2Cdescription_~Q3JlYXRlIGEgVnVlIHByb2plY3Qgd2l0aCBvbmUgY29tbWFuZCwgcmVhZCBBcHAudnVlIHRvcCB0byBib3R0b20sIGFuZCBidWlsZCBhIGNvdW50ZXIgdGhhdCB1cGRhdGVzIHRoZSBwYWdlLiBFdmVyeSBsaW5lIGV4cGxhaW5lZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy95b3VyLWZpcnN0LXZ1ZS1jb21wb25lbnQi%2Cs_czLkrC9pFRhsGVBi.png"
  "twitter:image:alt": "Your first Vue component, line by line · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Create the project

Node.js is the program that runs JavaScript outside a browser. The project scaffolder needs Node 18.3 or newer; if you are installing it now, pick 22, the active long-term support release. Then run one command in a folder where you keep projects:

```
npm create vue@latest
```

This downloads and runs `create-vue`, the official scaffolding tool. Version 3.13 is current. It prints "Vue.js - The Progressive JavaScript Framework" and asks a series of questions. Answer them like this for now:

- Project name: `hello-vue`
- Add TypeScript? Yes
- Add JSX Support? No
- Add Vue Router for Single Page Application development? No
- Add Pinia for state management? No
- Add Vitest for Unit Testing? No
- Add an End-to-End Testing Solution? No
- Add ESLint for code quality? No

Yes to TypeScript is the only answer that matters here. TypeScript adds types to JavaScript, so your editor can tell you about a mistake before you run anything. In VS Code, install the Vue extension the project recommends (`Vue.volar`, listed in `.vscode/extensions.json`); without it, the editor does not type-check `.vue` files. Everything else can be added later.

The 3.13 release also added a `--bare` flag: `npm create vue@latest -- --bare` asks the same questions but leaves out the example components, styles and logo. Keep it for a later project. This time, use the plain command, because the next sections read those example files.

## Start the development server

When create-vue finishes, it says "Done. Now run:" and shows three commands:

```
cd hello-vue
npm install
npm run dev
```

1. `cd hello-vue` moves into the new project folder.
2. `npm install` downloads the dependencies, including Vue 3.5 and Vite 6. Vite is the development server and build tool: it serves your files to the browser and updates the page when you save.
3. `npm run dev` starts Vite and prints a local address, usually `http://localhost:5173`.

Open that address and you will see a welcome page.

## How index.html and main.ts start the app

Open the project in your editor. The files that matter for now are in `src/`:

- `main.ts` creates the app and attaches it to the page.
- `App.vue` is the root component, the one everything else hangs off.
- `components/` holds the example components the welcome page is made of.
- `assets/` holds CSS and the logo.

`index.html` at the project root has one `<div id="app"></div>` and a script tag that loads `main.ts`. That file is short:

*src/main.ts*

```
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
```

The first line loads the global styles. `createApp(App)` builds a Vue application whose root component is `App`. `.mount('#app')` renders it into the div with that id. That is the whole bridge between the HTML page and Vue.

## Read App.vue top to bottom

A `.vue` file is called a single-file component, or SFC. It keeps the logic, the markup and the styles of one component in one file. The generated `App.vue` is longer than you need, so here is its shape, with the `<div class="wrapper">` around `HelloWorld` and most of the styles left out:

*src/App.vue*

```
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
    <HelloWorld msg="You did it!" />
  </header>

  <main>
    <TheWelcome />
  </main>
</template>

<style scoped>
header {
  line-height: 1.5;
}
</style>
```

Three blocks. Take them one at a time.

`<script setup lang="ts">` is where the logic lives. `setup` tells Vue that everything declared at the top level of this block is available in the template, with no exporting or returning. `lang="ts"` says the code is TypeScript. Here the script only imports two components. Importing a component is enough to use it as a tag.

`<template>` is the markup. It looks like HTML, and mostly it is HTML, with a few additions. `<HelloWorld msg="You did it!" />` uses the imported component as if it were an element and passes it a prop, which is a value handed from a parent to a child.

Notice there are two top-level elements, `<header>` and `<main>`. Vue 3 allows that. Vue 2 required a single root element, so older tutorials wrap the whole template in one `<div>`. In Vue 3 that wrapper is optional, and most of the time it only adds an extra div to your page.

`<style scoped>` is ordinary CSS. The `scoped` attribute means the `header` rule applies only to this component's elements, so a `header` in some other component is not affected.

## Build Counter.vue

Create a new file at `src/components/Counter.vue`:

*src/components/Counter.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">Clicked {{ count }} times</button>
</template>

<style scoped>
button {
  padding: 8px 16px;
  font-size: 1rem;
}
</style>
```

`ref(0)` creates a ref: a small object that holds a value (here the number 0) and lets Vue notice when that value changes. In script you read and change the value through `.value`, so `count.value++` adds one.

`@click="increment"` runs the function when the button is clicked. The `@` is short for `v-on:`, which listens for a DOM event.

`{{ count }}` is an interpolation: the double braces print a value as text. In the template Vue unwraps the ref for you, so you write `count`, not `count.value`.

Now use it. Replace the contents of `App.vue` with something smaller:

*src/App.vue*

```
<script setup lang="ts">
import Counter from './components/Counter.vue'
</script>

<template>
  <h1>Hello, Vue</h1>
  <Counter />
</template>
```

Save, look at the browser, click the button. The number goes up. You did not write any code that finds the button or sets its text. You changed a value, and Vue updated the page.

## What the compiler does with your template

A browser cannot run a `<template>` block. When Vite serves `Counter.vue`, its Vue plugin (`@vitejs/plugin-vue`, already in your project) compiles the file into plain JavaScript. The template becomes a render function: a function that returns a description of the DOM, which Vue calls a virtual DOM.

The compiler also works out what can change. In `<button @click="increment">Clicked {{ count }} times</button>`, only the button's text depends on data, so the compiler flags that text and nothing else. When `count` changes, Vue re-runs the render function and compares the result with the previous one, checking only the flagged parts. It then updates the button's text in the real page and leaves everything else alone.

That is why you do not write DOM updates yourself in Vue: you describe what the page should look like for a given state, and the compiled component does the rest.

## Two common mistakes with refs

Both are about `.value`. The first is forgetting it in script:

*Counter.vue**Breaks***

```
function increment() {
  count++
}
```

`count` is the ref object, not the number. `count++` tries to replace that object with a new value, which a `const` variable does not allow. With TypeScript and the Vue extension, your editor underlines it: `Cannot assign to 'count' because it is a constant.` Changing `const` to `let` only swaps that message for another, because the number you want to change lives in `count.value`.

Without TypeScript, the first sign may be the click itself: the number stays the same and the browser console shows a `TypeError` (Chrome words it `Assignment to constant variable.`). The fix is the line you saw earlier:

*Counter.vue**Works***

```
function increment() {
  count.value++
}
```

The second is writing `.value` in the template: `{{ count.value }}`. The template already unwrapped `count` into a number, and a number has no `value` property, so the page shows nothing where the count should be. In the template, leave `.value` off.

Change one thing before you move on. Make the button count down, or start at 10, or show "once" instead of "1 times". Each change you make is a small test of whether the three blocks make sense to you yet.

**Check it stuck**3 questions from the Your first component quiz.

[**Practice now**](https://vuejs.school/learn/beginner/your-first-component?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/your-first-vue-component#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-16","description":"Create a Vue project with one command, read App.vue top to bottom, and build a counter that updates the page. Every line explained.","name":"Your first Vue component, line by line","url":"https://vuejs.school/community/blog/your-first-vue-component","breadcrumb":{"@id":"https://vuejs.school/community/blog/your-first-vue-component#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/your-first-vue-component"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/your-first-vue-component#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-16","description":"Create a Vue project with one command, read App.vue top to bottom, and build a counter that updates the page. Every line explained.","headline":"Your first Vue component, line by line","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmU%2Cdescription_~Q3JlYXRlIGEgVnVlIHByb2plY3Qgd2l0aCBvbmUgY29tbWFuZCwgcmVhZCBBcHAudnVlIHRvcCB0byBib3R0b20sIGFuZCBidWlsZCBhIGNvdW50ZXIgdGhhdCB1cGRhdGVzIHRoZSBwYWdlLiBFdmVyeSBsaW5lIGV4cGxhaW5lZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy95b3VyLWZpcnN0LXZ1ZS1jb21wb25lbnQi%2Cs_czLkrC9pFRhsGVBi.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/your-first-vue-component#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/your-first-vue-component#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/your-first-vue-component#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Your first Vue component, line by line","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmU%2Cdescription_~Q3JlYXRlIGEgVnVlIHByb2plY3Qgd2l0aCBvbmUgY29tbWFuZCwgcmVhZCBBcHAudnVlIHRvcCB0byBib3R0b20sIGFuZCBidWlsZCBhIGNvdW50ZXIgdGhhdCB1cGRhdGVzIHRoZSBwYWdlLiBFdmVyeSBsaW5lIGV4cGxhaW5lZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy95b3VyLWZpcnN0LXZ1ZS1jb21wb25lbnQi%2Cs_czLkrC9pFRhsGVBi.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~WW91ciBmaXJzdCBWdWUgY29tcG9uZW50LCBsaW5lIGJ5IGxpbmU%2Cdescription_~Q3JlYXRlIGEgVnVlIHByb2plY3Qgd2l0aCBvbmUgY29tbWFuZCwgcmVhZCBBcHAudnVlIHRvcCB0byBib3R0b20sIGFuZCBidWlsZCBhIGNvdW50ZXIgdGhhdCB1cGRhdGVzIHRoZSBwYWdlLiBFdmVyeSBsaW5lIGV4cGxhaW5lZC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy95b3VyLWZpcnN0LXZ1ZS1jb21wb25lbnQi%2Cs_czLkrC9pFRhsGVBi.png"}]}
```