Getting Started
Hello World & Comments
A Vue 3 app starts with createApp() and mount() to a DOM element. Interpolation uses double curly braces {{ }} to render reactive data. HTML comments appear in the rendered DOM; for comments that don't leak, use /* */ inside <script> blocks.
<!-- index.html -->
<div id="app">
<p>{{ message }}</p>
<!-- This is an HTML comment, visible in DOM -->
</div>
<script type="module">
import { createApp } from 'vue'
createApp({
data() {
return { message: 'Hello Vue 3!' }
}
}).mount('#app')
</script>SFC Structure (Single File Component)
An SFC bundles template, script, and style in one .vue file. <script setup> is the recommended syntax for Composition API — concise and zero boilerplate. scoped styles only apply to the current component, preventing leakage. Each SFC compiles to a JavaScript module.
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<style scoped>
button { color: #42B883; }
</style>App Instance & Mount
createApp returns an application instance scoped to itself — no global Vue state, so you can mount multiple independent apps on one page. Register plugins, provide global values, and set config before calling mount(). After mount, further config changes won't take effect.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// global config / plugins before mounting
app.config.errorHandler = (err) => console.error(err)
app.provide('apiKey', '12345')
app.mount('#app')Project Scaffolding (Vite)
npm create vue@latest is the official scaffolding tool, offering optional TypeScript, Router, Pinia, and Vitest setup. It uses Vite for instant dev server start and HMR. create-vite is the lower-level tool if you want a minimal template without Vue's opinions.
# scaffold a new Vue project with Vite
npm create vue@latest my-app
cd my-app
npm install
npm run dev # start dev server (HMR)
npm run build # production build to /dist
# or with create-vite directly
npm create vite@latest my-app -- --template vue-tsdefineOptions & defineProps
defineProps, defineEmits, and defineOptions are compile-time macros — they don't need to be imported. defineProps declares the component's prop contract with runtime validation. defineOptions lets you set name, inheritAttrs, and other options that aren't expressible in <script setup> directly.
<script setup>
// declare props with types and defaults
const props = defineProps({
title: String,
count: { type: Number, default: 0 }
})
// declare emits
const emit = defineEmits(['update', 'delete'])
// additional non-reactive options
defineOptions({ name: 'MyButton', inheritAttrs: false })
</script>Template Syntax
Text Interpolation
{{ }} renders text safely (HTML-escaped). v-once freezes a binding after first render for static content. v-html injects raw HTML — only use on trusted content to avoid XSS. Inside {{ }} you can use any single JavaScript expression, but not statements like assignments or var declarations.
<template>
<p>{{ message }}</p>
<!-- renders the value of message -->
<!-- one-time binding (no updates after render) -->
<span v-once>{{ initialCount }}</span>
<!-- raw HTML (sanitize to avoid XSS!) -->
<div v-html="rawHtmlContent"></div>
<!-- expressions are supported -->
<p>{{ count * 2 }}</p>
<p>{{ isReady ? 'Yes' : 'No' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</template>Attribute Binding
v-bind (shorthand :) binds attributes reactively. For class, you can pass an object (truthy keys apply), array, or string. style accepts an object with camelCase or kebab-case keys. Dynamic argument syntax :[expr] lets the attribute name itself be reactive — useful for event names or localized attributes.
<template>
<!-- bind an attribute -->
<img v-bind:src="imageUrl" />
<img :src="imageUrl" /> <!-- shorthand -->
<!-- bind class -->
<div :class="{ active: isActive, error: hasError }"></div>
<div :class="['btn', isActive && 'active']"></div>
<!-- bind style -->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<!-- dynamic attribute name -->
<button :[eventName]="handler">Click</button>
</template>Boolean & Multi-value Attributes
For boolean attributes like disabled or checked, Vue renders the attribute only when the value is truthy. class and style are special: Vue merges static and bound values rather than replacing them. For form inputs, prefer v-model over manually binding :value and @input.
<template>
<!-- boolean attribute: presence depends on value -->
<input :disabled="isDisabled" />
<button :disabled="!canSubmit">Submit</button>
<!-- class merging: bound class merges with static class -->
<div class="card" :class="{ highlighted: isFeatured }"></div>
<!-- style merging: bound style merges with static style -->
<div style="color: red" :style="{ fontSize: '14px' }"></div>
<!-- form attributes: value is bound with v-model instead -->
<input v-model="text" />
</template>Modifiers & Expressions
Modifiers are postfix denoted by a dot, indicating special handling. Event modifiers (.prevent, .stop, .once, .self) wrap the handler with the corresponding DOM method. Key modifiers (.enter, .esc, .ctrl) filter events by key. v-model modifiers (.trim, .number, .lazy) transform the input value before assignment.
<template>
<!-- .prevent calls event.preventDefault() -->
<form @submit.prevent="onSubmit">...</form>
<!-- .stop calls event.stopPropagation() -->
<button @click.stop="doStuff">Click</button>
<!-- .once runs handler only one time -->
<button @click.once="init">Init</button>
<!-- .trim and .lazy modifiers on v-model -->
<input v-model.trim="email" />
<input v-model.lazy="text" />
<!-- chain multiple modifiers -->
<input @keyup.enter.ctrl="onCombo" />
</template>Template Refs
Template refs give direct access to DOM elements. Declare a ref() with the same name as the ref attribute — Vue assigns the DOM node after mount. Access inside onMounted (or later); before mount, the ref is null. For v-for, the ref becomes an array of elements. Avoid reaching for refs when reactive data flows would suffice.
<script setup>
import { ref, onMounted } from 'vue'
// declare a ref matching the template ref attribute
const inputEl = ref(null)
onMounted(() => {
// access the DOM node after mount
inputEl.value.focus()
})
</script>
<template>
<input ref="inputEl" />
</template>Directives
v-if / v-else-if / v-else
v-if conditionally renders elements — they are added/removed from the DOM. v-else-if and v-else chain with a preceding v-if. Wrap multiple elements in a <template v-if> when you don't want an extra wrapper element. v-if has higher toggle cost (DOM creation) but lower initial cost than v-show when the condition is false.
<template>
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else-if="type === 'C'">Type C</div>
<div v-else>Unknown type</div>
<!-- use <template> for invisible wrapper -->
<template v-if="showHeader">
<h1>Title</h1>
<p>Subtitle</p>
</template>
</template>v-show vs v-if
Use v-if when the condition rarely changes — elements are destroyed and recreated, saving render cost when hidden. Use v-show for frequently toggled elements (tabs, dropdowns) — the element stays in DOM and only display:none toggles, so toggling is cheap. v-show doesn't work with <template> or v-else.
<template>
<!-- v-if removes from DOM when false -->
<p v-if="isVisible">I'm created/destroyed</p>
<!-- v-show just toggles display:none -->
<p v-show="isVisible">I'm always in DOM, just hidden</p>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>v-for & :key
v-for iterates arrays, objects, or a number range. The :key attribute is REQUIRED for track-by identity — it lets Vue reuse DOM nodes efficiently during reordering. Use a stable unique id as key; never use the array index (breaks when items move). v-for has higher priority than v-if on the same element — avoid using both on one element.
<template>
<!-- iterate over array -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- iterate with index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
<!-- iterate over object -->
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
<!-- range (1 to n) -->
<span v-for="n in 10" :key="n">{{ n }}</span>
</template>v-model Bindings
v-model is two-way binding for form inputs: it syncs the input value with reactive state. A single checkbox binds to a boolean; multiple checkboxes with the same v-model bind to an array of checked values. Use modifiers: .lazy (sync on change instead of input), .number (cast to number), .trim (strip whitespace).