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:
- Consistency - Makes code easier to read and understand
- Maintainability - Structured code is easier to debug and extend
- Collaboration - Teams can work together more efficiently
- 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:
// This could conflict with a native HTML element
<template>
<div>Item component content</div>
</template>
<script>
export default {
name: 'Item'
}
</script>
✅ Good:
<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:
export default {
data: {
count: 0
}
}
✅ Good:
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:
export default {
props: ['status', 'title']
}
✅ Good:
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:
<template>
<ul>
<li v-for="item in items">{{ item.name }}</li>
</ul>
</template>
✅ Good:
<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:
<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:
<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
:
<template>
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
</li>
</template>
</ul>
</template>
Strongly Recommended Rules
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:
<!-- 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:
<!-- In a single-file component -->
<script>
export default {
name: 'myComponent'
}
</script>
✅ Good:
<!-- In a single-file component -->
<script>
export default {
name: 'MyComponent'
}
</script>
And in templates:
<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:
<!-- 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:
<!-- 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:
// 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:
<!-- 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>
<!-- 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:
<!-- 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:
- Name components with multiple words to avoid conflicts with HTML elements
- Always use a function for component data
- Define detailed props with types and validation
- Always use keys with v-for
- Keep v-if and v-for on separate elements
- Use Single-File Components for better organization
- Follow consistent naming conventions (PascalCase for components, camelCase for props in JavaScript)
- Prefix base components for clarity
- Follow the "props down, events up" pattern for component communication
- Structure your project in a logical way as it grows
Additional Resources
Exercises
- Refactor an existing Vue component to follow the style guide rules
- Create a set of base components for a project (BaseButton, BaseInput, etc.)
- Review a Vue project and identify any style guide violations
- 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! :)