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 <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

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, not Todo, so a component never collides with a current or future HTML element. (The root App and framework components like RouterView are the sanctioned exceptions.)

  • Detailed prop definitions — at minimum a type; in committed code also required or a default, and a validator where the value is constrained. Fully typed defineProps satisfies this.

  • Keyed v-for — always bind a stable, unique :key so Vue patches the list correctly and preserves component state.

  • Avoid v-if with v-for on the same element — their precedence is a trap and it re-evaluates the condition per iteration. Filter in a computed, or move the v-if to 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>

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; only App and 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 The prefix (TheHeader, TheSidebar).

  • Name components from most general to most specific — SearchButtonClear, not ClearSearchButton, 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, not UProf).

  • 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 the this-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