Setup and single-file components — the smallest setup to start with
The command to create a Vue project is a single line. It does ask a few questions along the way, though, and what you pick changes what gets generated. Some things can be added later and some are easier to decide up front, so knowing what you will be asked keeps you from hesitating.
This chapter creates a project with create-vue, the official scaffold, and reads through what each generated file is for. After that it looks at the structure of the single-file component, Vue's unit of development.
What you'll learn in this chapter
- Judge what the create-vue prompts ask and which options to pick
- Explain what the generated directories and config files are for
- Understand how to use the three blocks of a single-file component
Creating a project
This is the command the official Vue Quick Start points you to1.
npm create vue@latest
The Node.js requirement is ^22.18.0 || >=24.12.0. You need either 22.18.0 or later on the 22 line, or 24.12.0 or later, and the 23 line is out of scope. If you are on an odd-numbered line, switch.
You can also create a Vue project from a Vite template (vue-ts under npm create vite@latest), but what you get is minimal. create-vue combines the router, state management, type checking, and linter configuration for you, so you start closer to the setup you would use in production.
What the prompts ask
The questions come in this order.
| # | Question | Details |
|---|---|---|
| 1 | Project name (target directory) | The directory to create. Skipped if you passed it as a command argument |
| 2 | Package name | The name in package.json. Skipped if the directory name can be used as is |
| 3 | Use TypeScript? | Whether to use TypeScript. Asked before the feature selection |
| 4 | Select features to include in your project | Multi-select of features. See the table below |
| 5 | Select an End-to-End testing framework | Only if you chose End-to-End Testing in 4. Playwright or Cypress |
| 6 | Select experimental features to include in your project | Multi-select of experimental features. You can move on without picking any |
| 7 | Skip all example code and start with a blank Vue project? | Whether to strip the example code and start empty |
The feature selection in question 4 offers seven options.
| Option | Details | How this guide treats it |
|---|---|---|
| JSX Support | Lets you write JSX instead of templates | Not covered |
| Router (SPA development) | Adds Vue Router | Covered in chapter 15 and chapter 16 |
| Pinia (state management) | Adds the state management library | Covered in chapter 17 |
| Vitest (unit testing) | Adds a unit testing setup | Not covered in this guide. Vitest itself is covered in TypeScript Guide — Development toolchain |
| End-to-End Testing | Adds Playwright or Cypress | Not covered |
| Linter (error prevention) | Adds oxlint and ESLint | Not covered |
| Prettier (code formatting) | Adds code formatting | Not covered |
The experimental features in question 6 offer Vue 3.6 (Release Candidate) and an option to swap Prettier for Oxfmt. This guide takes the stable 3.5 as its baseline, so it picks neither. The question always appears when you go through the prompts, so if you do not need them, move on without selecting anything.
If you want to start with the smallest possible setup for learning, enabling only TypeScript and leaving the rest off is the clearest. This guide covers Router and Pinia later on, so if you are reading straight through, including them from the start keeps the diffs small.
Skipping the prompts
Passing flags skips the prompts. This is useful in CI, or when recreating the same setup.
# Create a TypeScript + Router + Pinia setup without the prompts
npm create vue@latest my-app -- --ts --router --pinia
--default gives you a setup with no extra features, but TypeScript is still enabled. --bare tells it to omit the example code and is not a feature flag, so passing it on its own still starts the prompts.
Installing dependencies and starting up
cd my-app
npm install
npm run dev
Reading what was generated
Here is the layout when you pick TypeScript, Router, and Pinia.
my-app/
├── index.html The entry HTML. This is where #app lives
├── vite.config.ts Vite config. The @ → ./src alias is here too
├── env.d.ts Declarations for the types Vite injects
├── tsconfig.json A parent that only references the split configs
├── tsconfig.app.json For code that runs in the browser
├── tsconfig.node.json For code that runs in Node, such as config files
├── public/ Files served as-is without transformation
└── src/
├── main.ts Assembles the app and mounts it on #app
├── App.vue The top-level component
├── assets/ CSS and images. Transformed at build time
├── components/ Reusable components
├── views/ Screen-level components mapped to routes
├── router/ Route definitions
└── stores/ Pinia stores
This grows and shrinks with the features you pick. Choosing Linter adds eslint.config.ts and .oxlintrc.json; Prettier adds .prettierrc.json. Vitest adds vitest.config.ts and tsconfig.vitest.json, bringing the configs referenced by the parent to three.
composables/ is not generated. It is a directory for extracted logic, so you create it yourself when you need it (covered in chapter 8).
The entry point
src/main.ts is where the app gets assembled.
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
createApp() creates the app instance, use() registers plugins, and mount() inserts it into #app in index.html. When you add a router or state management, you add another use() here.
Why tsconfig is split
tsconfig.json contains nothing but references; the actual settings are split up. Code that runs in the browser and code that runs in Node, such as vite.config.ts, have different APIs and types available. Mixing them would let a reference to window pass in a place where window does not exist, or conversely leave Node APIs unusable.
The app-side tsconfig.app.json includes noUncheckedIndexedAccess. Types are assigned on the assumption that indexed access can yield undefined, so the type of const first = items[0] is string | undefined. Merely reading it is not an error, but using it somewhere that does not allow undefined, such as items[0].length, stops the build. It is a setting that errs on the safe side.
Type checking runs with npm run type-check. Underneath it is vue-tsc --build, which processes the split configs together. It uses vue-tsc rather than plain tsc because the contents of .vue files have to be parsed.
The commands you get
| Command | Details |
|---|---|
npm run dev | Starts the dev server |
npm run build | Runs type checking and the build together, output to dist/ |
npm run build-only | Skips type checking and only builds |
npm run preview | Serves the build output locally so you can check it |
npm run type-check | Runs only the type check |
npm run test:unit | Runs unit tests with Vitest (if you chose Vitest) |
npm run lint | Runs oxlint and then ESLint (if you chose Linter) |
npm run format | Formats with Prettier (if you chose Prettier) |
Choosing Linter makes lint a two-stage setup that calls oxlint and then ESLint. The ESLint side is configured to disable rules that overlap with oxlint, so the same problem is not reported twice.
The three blocks of a single-file component
A Vue component is a single file with a .vue extension, holding the template, the logic, and the styles in the same place.
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('Vue')
</script>
<template>
<p class="greeting">Hello, {{ name }}</p>
</template>
<style scoped>
.greeting {
font-weight: bold;
}
</style>
The three blocks play these roles.
| Block | Role |
|---|---|
<script setup lang="ts"> | The component's logic. Variables and functions declared here are usable from the template |
<template> | The structure of the screen. HTML plus Vue's own notation |
<style scoped> | Styles for this component |
The order is not fixed. What create-vue generates puts <script setup> first, so this guide follows that.
What script setup leaves out
With <script setup>, anything declared at the top level is usable from the template. You do not need to write a return. Imported components are usable directly too, so no registration is needed.
<script setup lang="ts">
import Greeting from './Greeting.vue'
</script>
<template>
<Greeting />
</template>
What scoped does
With <style scoped>, Vue assigns a unique attribute per component to the elements and adds that attribute to the selectors. This means you do not have to manage class name collisions yourself, so you can give each component plain, obvious names.
The scope is not "only inside this component". A child component that returns a single root element also gets the parent's attribute on that root element. If the child has its own <style scoped>, both attributes are applied; if it does not, only the parent's is. Either way, the parent's selector matches that root element. When the child returns multiple root elements, on the other hand, none of them receive the parent's attribute.
The parent's styles reaching a single-root child is useful when the parent wants to decide layout spacing. It can also change the child's appearance unintentionally, so it is worth keeping this route in mind.
To style something inside the child (deeper than its root), pierce through with :deep(). That does make the code depend on the child's internal structure, so overusing it makes things fragile under change.
What to do about Vue CLI
Vue has an older webpack-based toolchain called Vue CLI, but the official position is that it is in maintenance mode. New projects are recommended to start with Vite, with an exception only where webpack-specific features are required2.
That does not mean projects already running on Vue CLI will break. But when creating something new, use create-vue.
Editor setup
create-vue writes recommended extensions into .vscode/extensions.json. The central one is Vue - Official (identifier Vue.volar), which handles parsing and type information for .vue files. It used to be called Volar; it is now shipped as the official Vue extension.
Summary
- Create projects with
npm create vue@latest. A Vite template works too, but the official scaffold is create-vue - Node must be 22.18.0 or later on the 22 line, or 24.12.0 or later; the 23 line is out of scope
- The prompts ask whether to use TypeScript first, with the multi-select of features after that. The experimental-features question always appears in the interactive flow, so if you are not using the 3.6 candidate release, move on without selecting anything
- Flags let you skip the prompts. Note that
--defaultstill enables TypeScript, and--bareon its own does not stop the prompts tsconfig.jsonis split because the browser and Node have different APIs available. The app side includesnoUncheckedIndexedAccess- Type checking is handled by
vue-tsc. The contents of.vuefiles have to be parsed, so plaintscis not enough - A single-file component has three blocks:
<script setup>,<template>, and<style scoped>. Using<script setup>removes the need forreturnand for component registration - The reach of
scopedis not limited to the inside of the component. A single-root child gets the parent's attribute on that root element (a multi-root child does not) - Vue CLI is in maintenance mode, not discontinued. Do not use it for new projects
- Vue — Quick Start — the official steps and the Node requirement
- Vue — Tooling — where create-vue and Vue CLI stand
- Vue — SFC Syntax Specification — the block specification
- Tailwind CSS guide — the sister guide that goes deep on styling
What to read next
- Reactivity — how a change to a declared value reaches the screen
- Template syntax and directives — the notation you can write in
<template>