Skip to main content

Vue.js Style Guide

Introduction

The Vue.js Style Guide is a set of standards and recommendations for writing clean, maintainable Vue code. Following these conventions not only helps individual developers write better code, but it also creates consistency across projects and teams.

This guide is based on the official Vue.js style guide, adapted and simplified for beginners. By following these recommendations from the start of your Vue journey, you'll develop good habits that will benefit your projects as they grow in complexity.

Why Follow a Style Guide?

Before diving into the specific rules, it's important to understand why following a style guide matters:

  1. Consistency - Makes code easier to read and understand
  2. Maintainability - Structured code is easier to debug and extend
  3. Collaboration - Teams can work together more efficiently
  4. Quality - Many style rules help prevent common bugs

Essential Rules

These rules help prevent errors and should be considered mandatory.

Component Names: Multi-Word

Rule: Always use multi-word names for components except for root App components.

Why? This prevents conflicts with existing and future HTML elements, as all HTML tags are single words.

Bad:

html
// This could conflict with a native HTML element
<template>
<div>Item component content</div>
</template>

<script>
export default {
name: 'Item'
}
</script>

Good:

html
<template>
<div>Product item component content</div>
</template>

<script>
export default {
name: 'ProductItem'
}
</script>

Component Data: Must Be a Function

Rule: The data property in components should always be a function that returns an object.

Why? When using a plain object, that same object will be shared across all instances of the component. Using a function ensures each component instance maintains its own separate data.

Bad:

js
export default {
data: {
count: 0
}
}

Good:

js
export default {
data() {
return {
count: 0
}
}
}

Props: Detailed Definitions

Rule: Prop definitions should be as detailed as possible, specifying at minimum the type.

Why? Detailed prop definitions serve as documentation and warn developers about incorrect usage.

Bad:

js
export default {
props: ['status', 'title']
}

Good:

js
export default {
props: {
status: {
type: String,
required: true,
validator: value => ['success', 'warning', 'error'].includes(value)
},
title: {
type: String,
default: 'Untitled'
}
}
}

v-for with Key

Rule: Always use key with v-for.

Why? This helps Vue identify each node in the list, making DOM updates more efficient and predictable.

Bad:

html
<template>
<ul>
<li v-for="item in items">{{ item.name }}</li>
</ul>
</template>

Good:

html
<template>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>

Avoid v-if with v-for

Rule: Never use v-if on the same element as v-for.

Why? v-for has a higher priority than v-if, so the v-if will be run on each iteration, which is inefficient.

Bad:

html
<template>
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>

Good - Option 1: Filter the list first:

html
<template>
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>

<script>
export default {
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
}
</script>

Good - Option 2: Use a wrapper element with v-if:

html
<template>
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
</li>
</template>
</ul>
</template>

These rules improve readability and developer experience.

Component File Organization

Rule: Use Single-File Components (.vue files) for organizing your code.

Why? Single-File Components keep template, script, and styles together in one file, making components easier to understand and maintain.

Good:

html
<!-- UserProfile.vue -->
<template>
<div class="user-profile">
<h2>{{ user.name }}</h2>
<p>{{ user.bio }}</p>
</div>
</template>

<script>
export default {
props: {
user: {
type: Object,
required: true
}
}
}
</script>

<style scoped>
.user-profile {
padding: 20px;
border: 1px solid #eee;
}
</style>

Component Name Casing

Rule: Use PascalCase for component names in single-file components and JS/TS files. Use kebab-case in templates.

Bad:

html
<!-- In a single-file component -->
<script>
export default {
name: 'myComponent'
}
</script>

Good:

html
<!-- In a single-file component -->
<script>
export default {
name: 'MyComponent'
}
</script>

And in templates:

html
<template>
<div>
<my-component></my-component>
</div>
</template>

Base Component Names

Rule: Base components that are used throughout your application should start with a specific prefix, such as Base, App, or V.

Why? This makes it clear which components are reusable building blocks.

Good:

components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
|- AppHeader.vue
|- AppFooter.vue

Example of a base component:

html
<!-- BaseButton.vue -->
<template>
<button class="base-button" :class="type" @click="$emit('click')">
<slot></slot>
</button>
</template>

<script>
export default {
name: 'BaseButton',
props: {
type: {
type: String,
default: 'default'
}
}
}
</script>

<style scoped>
.base-button {
padding: 8px 16px;
border-radius: 4px;
font-size: 16px;
}
.base-button.primary {
background-color: #4CAF50;
color: white;
}
.base-button.default {
background-color: #e0e0e0;
color: #333;
}
</style>

Prop Naming

Rule: Always use camelCase for prop names in your JavaScript, and kebab-case in your templates.

Why? This follows JavaScript and HTML conventions.

Good:

html
<!-- In a component -->
<script>
export default {
props: {
greetingMessage: String
}
}
</script>

<!-- In a parent component -->
<template>
<WelcomeBanner greeting-message="Hello World!" />
</template>

Organization and Component Communication

Vuex for State Management

For applications with multiple components that need to share state, use Vuex for state management.

Example Vuex Store:

js
// store/index.js
import { createStore } from 'vuex'

export default createStore({
state() {
return {
count: 0,
todos: []
}
},
mutations: {
increment(state) {
state.count++
},
addTodo(state, todo) {
state.todos.push(todo)
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
completedTodos(state) {
return state.todos.filter(todo => todo.completed)
}
}
})

Component Communication Patterns

Props Down, Events Up

For parent-child communication:

  • Parent components pass data to children via props
  • Child components communicate with parents by emitting events

Example:

html
<!-- Parent.vue -->
<template>
<div>
<ChildComponent
:message="parentMessage"
@update="handleUpdate"
/>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
components: {
ChildComponent
},
data() {
return {
parentMessage: 'Hello from parent'
}
},
methods: {
handleUpdate(newValue) {
console.log('Child updated with:', newValue)
}
}
}
</script>
html
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ message }}</p>
<button @click="updateParent">Update Parent</button>
</div>
</template>

<script>
export default {
props: {
message: {
type: String,
required: true
}
},
methods: {
updateParent() {
this.$emit('update', 'New data from child')
}
}
}
</script>

Practical Application: Building with Style

Let's see these guidelines in action by building a simple Todo app component:

html
<!-- TodoApp.vue -->
<template>
<div class="todo-app">
<BaseInput
v-model="newTodo"
placeholder="Add a new task"
@keyup.enter="addTodo"
/>

<BaseButton @click="addTodo" :disabled="!newTodo.trim()">
Add Task
</BaseButton>

<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:class="{ 'completed': todo.completed }"
>
<BaseCheckbox v-model="todo.completed" />
<span class="todo-text">{{ todo.text }}</span>
<BaseButton
class="delete-btn"
@click="removeTodo(todo.id)"
type="danger"
>
Delete
</BaseButton>
</li>
</ul>

<div class="todo-footer" v-if="todos.length">
<p>{{ activeCount }} items left</p>
<BaseButton @click="clearCompleted" v-if="completedCount">
Clear completed
</BaseButton>
</div>
</div>
</template>

<script>
import BaseInput from './BaseInput.vue'
import BaseButton from './BaseButton.vue'
import BaseCheckbox from './BaseCheckbox.vue'

export default {
name: 'TodoApp',
components: {
BaseInput,
BaseButton,
BaseCheckbox
},
data() {
return {
newTodo: '',
todos: []
}
},
computed: {
activeCount() {
return this.todos.filter(todo => !todo.completed).length
},
completedCount() {
return this.todos.filter(todo => todo.completed).length
}
},
methods: {
addTodo() {
const text = this.newTodo.trim()
if (text) {
this.todos.push({
id: Date.now(),
text,
completed: false
})
this.newTodo = ''
}
},
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
},
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.completed)
}
}
}
</script>

<style scoped>
.todo-app {
max-width: 400px;
margin: 0 auto;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.todo-list {
list-style-type: none;
padding: 0;
}

.todo-list li {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}

.todo-text {
flex-grow: 1;
margin: 0 10px;
}

.completed .todo-text {
text-decoration: line-through;
color: #999;
}

.todo-footer {
display: flex;
justify-content: space-between;
margin-top: 20px;
font-size: 14px;
}

.delete-btn {
font-size: 12px;
padding: 4px 8px;
}
</style>

This example demonstrates:

  • Multi-word component names (TodoApp)
  • Base components for reusability (BaseInput, BaseButton)
  • Proper data function
  • v-for with key
  • Proper event handling
  • Clean separation of concerns

Project Structure

For larger Vue applications, consider the following structure:

src/
|-- assets/ # Static assets like images, fonts
|-- components/
| |-- base/ # Base components (Button, Input, etc.)
| |-- common/ # Common components used across features
| `-- features/ # Feature-specific components
|-- composables/ # Reusable composition functions
|-- views/ # Page components
|-- router/ # Vue Router configuration
|-- store/ # Vuex store modules
|-- services/ # API services
|-- utils/ # Utility functions
`-- App.vue # Root component

This structure helps organize your code as your application grows.

Summary

Following the Vue.js style guide leads to cleaner, more maintainable code, and helps you avoid common pitfalls. As you progress in your Vue journey, these practices will become second nature and contribute significantly to the quality of your applications.

Key takeaways:

  1. Name components with multiple words to avoid conflicts with HTML elements
  2. Always use a function for component data
  3. Define detailed props with types and validation
  4. Always use keys with v-for
  5. Keep v-if and v-for on separate elements
  6. Use Single-File Components for better organization
  7. Follow consistent naming conventions (PascalCase for components, camelCase for props in JavaScript)
  8. Prefix base components for clarity
  9. Follow the "props down, events up" pattern for component communication
  10. Structure your project in a logical way as it grows

Additional Resources

Exercises

  1. Refactor an existing Vue component to follow the style guide rules
  2. Create a set of base components for a project (BaseButton, BaseInput, etc.)
  3. Review a Vue project and identify any style guide violations
  4. Create a component that demonstrates proper props validation and component communication

By following these guidelines consistently, you'll write Vue code that's not only more maintainable but also more intuitive for other developers (and your future self) to understand and extend.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)