Style Guide and Best Practices
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
The official Vue Style Guide ranks conventions by how much trouble ignoring them causes, and the Composition API FAQ explains why the modern authoring style is shaped the way it is. This page summarises both and adds component-design guidance that the style guide deliberately leaves out.
The Style Guide priority levels
The guide sorts every rule into four buckets. In practice you enforce the first two and treat the third as a team decision.
Priority A — Essential (error prevention)
Breaking these invites real bugs. See Essential rules.
-
Multi-word component names —
TodoItem, notTodo, so a component never collides with a current or future HTML element. (The rootAppand framework components likeRouterVieware the sanctioned exceptions.) -
Detailed prop definitions — at minimum a type; in committed code also
requiredor adefault, and avalidatorwhere the value is constrained. Fully typeddefinePropssatisfies this. -
Keyed
v-for— always bind a stable, unique:keyso Vue patches the list correctly and preserves component state. -
Avoid
v-ifwithv-foron the same element — their precedence is a trap and it re-evaluates the condition per iteration. Filter in a computed, or move thev-ifto a wrapper.
<script setup lang="ts">
interface Task { id: number; title: string; done: boolean }
const props = defineProps<{ tasks: Task[] }>()
const pending = computed(() => props.tasks.filter(t => !t.done))
</script>
<template>
<ul>
<TaskRow v-for="task in pending" :key="task.id" :task="task" />
</ul>
</template>
Priority B — Strongly Recommended (readability)
Improve consistency across a codebase. See Strongly Recommended rules.
-
Component-scoped styling —
<style scoped>in SFCs, CSS Modules, or a class-based convention like BEM; onlyAppand layout-level elements get truly global styles. -
Component files — one component per file, named in
PascalCase.vue. -
Base/presentational components share a prefix (
BaseButton,AppButton,VButton). -
Single-instance components get a
Theprefix (TheHeader,TheSidebar). -
Name components from most general to most specific —
SearchButtonClear, notClearSearchButton, so related files sort together. -
Self-close components with no content —
<UserCard />in SFC templates. -
PascalCase component names in templates, and full words over abbreviations (
UserSettings, notUProf). -
Prefer full directive names in shared code (
v-bind:,v-on:); the:/@shorthands are fine but apply them consistently. -
Order the SFC blocks
<script>,<template>,<style>(or<template>first) — consistently across the project. -
Multi-attribute elements span multiple lines, one attribute per line.
-
Keep template expressions simple — move anything beyond a property access or a single call into a computed or method.
Priority C — Recommended (arbitrary but consistent)
Where more than one option is equally fine, pick one project-wide. See
Recommended rules: a consistent order for component
options / <script setup> contents, and using an element-plus-attribute selector (input[type="text"])
sparingly.
Earlier editions also carried a Priority D ("Use with caution") list — v-if/v-else without key,
scoped selectors that reach into children — now folded into the pages above.
Enforce A and B automatically with eslint-plugin-vue (plugin:vue/vue3-recommended).
Component design
The style guide stops at mechanics; these principles decide where a component boundary should go.
Single responsibility
A component should do one thing. When setup grows past a screen or so, or a name needs "and" to describe
it, split it — extract child components for distinct UI regions and
composables for reusable stateful logic.
Props down, events up
Data flows one way: a parent passes state down as props, a child requests change by emitting an event. A
child never mutates a prop (Vue warns) and never reaches into $parent.
<script setup lang="ts">
const props = defineProps<{ quantity: number }>()
const emit = defineEmits<{ 'update:quantity': [value: number] }>()
// v-model:quantity on the parent wires these together
</script>
<template>
<button @click="emit('update:quantity', props.quantity - 1)">-</button>
<span>{{ quantity }}</span>
<button @click="emit('update:quantity', props.quantity + 1)">+</button>
</template>
Slots vs. props
Pass data as props; pass markup as slots. If a prop’s value is a string of HTML, or you find
yourself adding iconBefore / iconAfter / footerText props, switch to named and scoped slots. See
Slots.
<template>
<Card>
<template #header><h2>{{ title }}</h2></template>
<p>{{ body }}</p>
<template #actions="{ close }">
<button @click="close">Dismiss</button>
</template>
</Card>
</template>
Container vs. presentational
Separate components that fetch and own state (containers: talk to the store, the router, the API) from components that only render props and emit events (presentational: no I/O, trivial to test and to reuse). Keep presentational components the majority.
Folder structure
A common layout: components/ for shared presentational components, views/ (or pages/) for
route-level containers, composables/ for useX functions, stores/ for Pinia stores, and feature
folders that co-locate a feature’s own components, composable, and store. Consistency matters more than the
exact names — see Single-File Components.
Why the Composition API
The Composition API FAQ gives the rationale, in short:
-
Logic reuse — composables replace mixins and renderless components without their namespace clashes, implicit dependencies, or wrapper-component overhead.
-
Better organisation — code for one concern (search, pagination, drag-and-drop) sits together in one composable instead of being scattered across
data/methods/watch/ lifecycle options. -
Type inference —
ref,computed, props, and emits are plain values and functions, so TypeScript infers them without thethis-typing machinery the Options API needs (see Vue and TypeScript). -
Smaller production bundles —
<script setup>code minifies better and its variable names can be mangled.
The FAQ is explicit that the Options API is not deprecated and remains a good fit for smaller apps and
for developers who prefer its structure. New code and libraries in the ecosystem default to
<script setup>, which is why every example in this section uses it.
See also
-
Vue and TypeScript — typed
defineProps/defineEmitssatisfy the "detailed prop definitions" rule. -
Performance and Deployment — keyed
v-forand simple template expressions are also performance rules. -
React Reference and Angular Reference — how the other frameworks frame component design.