Skip to content

Vue.js Style Guide

Rules

Use multi-word component names [Mandatory]

User component names should always be multi-word, except for root App components. This prevents conflicts with existing and future HTML elements, since all HTML elements are a single word.

// bad
<!-- in pre-compiled templates -->
<Item />

<!-- in in-DOM templates -->
<item></item>

// good
<!-- in pre-compiled templates -->
<TodoItem />

<!-- in in-DOM templates -->
<todo-item></todo-item>

Use detailed prop definitions [Mandatory]

In committed code, prop definitions should always be as detailed as possible, specifying at least type(s).

// bad
// This is only OK when prototyping
props: ['status']

// good
// Even better!
props: {
  status: {
    type: String,
    required: true,

    validator: value => {
      return [
        'syncing',
        'synced',
        'version-conflict',
        'error'
      ].includes(value)
    }
  }
}

Use keyed v-for [Mandatory]

key with v-for is always required on components, in order to maintain internal component state down the subtree. Even for elements though, it's a good practice to maintain predictable behavior, such as object constancy in animations.

// bad
<ul>
  <li v-for="todo in todos">
    {{ todo.text }}
  </li>
</ul>

// good
<ul>
  <li
    v-for="todo in todos"
    :key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

Avoid v-if with v-for [Mandatory]

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

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

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

Use component-scoped styling [Mandatory]

For applications, styles in a top-level App component and in layout components may be global, but all other components should always be scoped.

// bad
<template>
  <button class="btn btn-close">×</button>
</template>

<style>
.btn-close {
  background-color: red;
}
</style>

// good
<template>
  <button class="button button-close">×</button>
</template>

<!-- Using the `scoped` attribute -->
<style scoped>
.button {
  border: none;
  border-radius: 2px;
}

.button-close {
  background-color: red;
}
</style>

Avoid exposing private functions in mixins [Mandatory]

Always use the $_ prefix for custom private properties in a plugin, mixin, etc that should not be considered public API. Then to avoid conflicts with code by other authors, also include a named scope (e.g. $yourPluginName).

// bad
const myGreatMixin = {
  // ...
  methods: {
    update() {
      // ...
    }
  }
}
const myGreatMixin = {
  // ...
  methods: {
    $update() {
      // ...
    }
  }
}

// good
const myGreatMixin = {
  // ...
  methods: {
    $_myGreatMixin_update() {
      // ...
    }
  }
}
// Even better!
const myGreatMixin = {
  // ...
  methods: {
    publicMethod() {
      // ...
      myPrivateFunction()
    }
  }
}

function myPrivateFunction() {
  // ...
}

export default myGreatMixin

Whenever a build system is available to concatenate files, each component should be in its own file.

// bad
app.component('TodoList', {
  // ...
})

app.component('TodoItem', {
  // ...
})

// good
components/
|- TodoList.js
|- TodoItem.js
components/
|- TodoList.vue
|- TodoItem.vue

Single-file component filename casing [Mandatory]

Filenames of Single-File Components should either be always PascalCase or always kebab-case. PascalCase works best with autocompletion in code editors, as it's consistent with how we reference components in JS(X) and templates, wherever possible. However, mixed case filenames can sometimes create issues on case-insensitive file systems, which is why kebab-case is also perfectly acceptable.

💡 Filenames of Single-File Components should be PascalCase in MCT and UAS projects.

// bad
components/
|- mycomponent.vue
components/
|- myComponent.vue

// good
components/
|- MyComponent.vue
components/
|- my-component.vue

Base component names [Mandatory]

Base components (a.k.a. presentational, dumb, or pure components) that apply app-specific styling and conventions should all begin with a specific prefix, such as Base, App, or V.

// bad
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue

// good
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
components/
|- AppButton.vue
|- AppTable.vue
|- AppIcon.vue
components/
|- VButton.vue
|- VTable.vue
|- VIcon.vue

Single-instance component names [Reference]

Components that should only ever have a single active instance should begin with the The prefix, to denote that there can be only one.

This does not mean the component is only used in a single page, but it will only be used once per page. These components never accept any props, since they are specific to your app, not their context within your app. If you find the need to add props, it's a good indication that this is actually a reusable component that is only used once per page for now.

// bad
components/
|- Heading.vue
|- MySidebar.vue

// good
components/
|- TheHeading.vue
|- TheSidebar.vue

Child components that are tightly coupled with their parent should include the parent component name as a prefix.

If a component only makes sense in the context of a single parent component, that relationship should be evident in its name. Since editors typically organize files alphabetically, this also keeps these related files next to each other.

// bad
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- SearchSidebar.vue
|- NavigationForSearchSidebar.vue

// good
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
components/
|- SearchSidebar.vue
|- SearchSidebarNavigation.vue

Component names should start with the highest-level (often most general) words and end with descriptive modifying words.

// bad
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue

// good
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue

Component names should prefer full words over abbreviations.

The autocompletion in editors make the cost of writing longer names very low, while the clarity they provide is invaluable. Uncommon abbreviations, in particular, should always be avoided.

// bad
components/
|- SdSettings.vue
|- UProfOpts.vue

// good
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue

Prop names should always use camelCase during declaration, but kebab-case in templates and JSX.

We're simply following the conventions of each language. Within JavaScript, camelCase is more natural. Within HTML, kebab-case is.

// bad
props: {
  'greeting-text': String
}
<WelcomeMessage greetingText="hi"/>

// good
props: {
  greetingText: String
}
<WelcomeMessage greeting-text="hi"/>

Elements with multiple attributes should span multiple lines, with one attribute per line.

In JavaScript, splitting objects with multiple properties over multiple lines is widely considered a good convention, because it's much easier to read. Our templates and JSX deserve the same consideration.

// bad
<img src="https://vuejs.org/images/logo.png" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>

// good
<img
  src="https://vuejs.org/images/logo.png"
  alt="Vue Logo"
>
<MyComponent
  foo="a"
  bar="b"
  baz="c"
/>

Directive shorthands (: for v-bind:, @ for v-on: and # for v-slot) should be used always or never.

// bad
<input
  v-bind:value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-on:input="onInput"
  @focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>
// good
<input
  :value="newTodoText"
  :placeholder="newTodoInstructions"
>
<input
  v-bind:value="newTodoText"
  v-bind:placeholder="newTodoInstructions"
>
<input
  @input="onInput"
  @focus="onFocus"
>
<input
  v-on:input="onInput"
  v-on:focus="onFocus"
>
<template v-slot:header>
  <h1>Here might be a page title</h1>
</template>

<template v-slot:footer>
  <p>Here's some contact info</p>
</template>
<template #header>
  <h1>Here might be a page title</h1>
</template>

<template #footer>
  <p>Here's some contact info</p>
</template>

Setting in project

If there is .eslintrc.{js,yml,json} in your project, you can add 'plugin:vue/vue3-essential' as extension.

extends: ['plugin:vue/vue3-essential']

Then, add some lints in rules.

'vue/attribute-hyphenation': ['error', 'always' ],
'vue/first-attribute-linebreak': ['error', {
  singleline: 'ignore',
  multiline: 'below'
}],
'vue/html-closing-bracket-spacing': ['error'],
'vue/mustache-interpolation-spacing': ['error', 'always'],
'vue/no-multi-spaces': ['error', {
  ignoreProperties: false
}],
'vue/no-spaces-around-equal-signs-in-attribute': ['error'],
'vue/one-component-per-file': ['error'],
'vue/prop-name-casing': ['error', 'camelCase'],
'vue/require-default-prop': ['error'],
'vue/require-explicit-emits': ['error'],
'vue/require-prop-types': ['error'],
'vue/v-bind-style': ['error', 'shorthand'],
'vue/v-on-style': ['error', 'shorthand'],
'vue/v-slot-style': ['error', {
  atComponent: 'shorthand',
  default: 'shorthand',
  named: 'shorthand'
}],
'vue/no-v-html': ['error']

Else, running npx eslint --init to get a .eslintrc.{js,yml,json} file.

References

Official Vue.js Style Guide

Vue Eslint Plugin