Vue Single-File Component Formatter

Paste a whole .vue file and Prettier formats each block with the right parser: HTML rules for the template, JavaScript or TypeScript for the script, CSS or SCSS for the style.

Input

Settings

History

Load from URL

One file, three languages

A Vue single-file component mixes three languages, and that is exactly why a generic HTML beautifier mangles it. Prettier’s Vue support splits the file into its blocks and formats each one properly:

  • The <template> is printed with HTML rules, and the JavaScript inside it is formatted too: {{count===1?'click':'clicks'}} gains spaces, :class="{invalid:errors.email}" becomes :class="{ invalid: errors.email }", and elements with many attributes are broken one attribute per line.
  • <script> and <script setup> are formatted as JavaScript, or as TypeScript when the tag says lang="ts", so defineProps<{ user?: User }>() and interfaces work.
  • <style> blocks go through Prettier’s CSS printer, including lang="scss" and lang="less".

Blocks Prettier has no parser for are passed through untouched. That covers <template lang="pug"> and custom blocks such as <i18n> or <docs>. Script and style contents are not indented under their tags, which is Prettier’s default for Vue files.

Script options

The options are Prettier’s JavaScript settings, so they apply to the script block and to expressions in the template.

  • Semicolons: on by default. Turn it off for the no-semicolon style common in Vue projects created with older templates.
  • Single quotes: switches string literals in the script and inside {{ }} to single quotes. Attribute values in the template always keep their double quotes.
  • Trailing commas: All adds a comma after the last item of multi-line arrays, objects, parameters and arguments; ES5 only in arrays and objects; None never.
  • Spaces inside { }: writes { ref, computed } rather than {ref, computed}, in imports, object literals and template bindings.
  • Arrow function parentheses: Always gives (item) => item.id, Avoid gives item => item.id when there is a single parameter.

Indent in the toolbar sets the indentation for all three blocks at once.

Where errors come from

The template parser is strict about structure. A closing tag that does not match what is open, such as </template> arriving while a <div> inside it is still open, produces an “Unexpected closing tag” error with its position. Some mistakes are quietly repaired instead, following browser rules: a <p> left open is closed for you when its parent’s </div> arrives.

Embedded code is more forgiving. If the script or style block has a syntax error, Prettier leaves that block exactly as written and formats the rest, so nothing is reported. If a block you expected to change comes out untouched, check it for a typo. The same applies to a broken expression in an interpolation.

Using the formatter

Input that starts with <template> and also contains a script or style block is detected as Vue on its own. Ctrl/Cmd+Enter formats on demand and Ctrl/Cmd+Shift+C copies the result. All parsing happens locally in the page, and nothing is sent anywhere while you work.

Lines wrap at 80 columns. A template holding nothing but one short element of plain text, such as <template><p>Loading</p></template>, may stay on a single line. That comes from Prettier’s whitespace rules for inline text and is not a sign that formatting failed; add an interpolation or a nested element and it expands as usual.

Examples

Script setup with TypeScript, no semicolons

The template, the TypeScript script and the scoped CSS are each formatted with their own parser.

Input
<template><div class="profile"><p v-if="user">{{user.name}} ({{user.email}})</p><p v-else>Guest</p><button @click="emit('logout')" :disabled="!user">Log out</button></div></template>
<script setup lang="ts">
import {computed} from 'vue'
interface User{id:number;name:string;email:string}
const props=defineProps<{user?:User}>()
const emit=defineEmits<{(e:'logout'):void}>()
const label=computed(()=>props.user?.name??'Guest')
</script>
<style scoped>.profile{display:flex;gap:.5rem;align-items:center}</style>
Output
<template>
  <div class="profile">
    <p v-if="user">{{ user.name }} ({{ user.email }})</p>
    <p v-else>Guest</p>
    <button @click="emit('logout')" :disabled="!user">Log out</button>
  </div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface User {
  id: number
  name: string
  email: string
}
const props = defineProps<{ user?: User }>()
const emit = defineEmits<{ (e: 'logout'): void }>()
const label = computed(() => props.user?.name ?? 'Guest')
</script>
<style scoped>
.profile {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
</style>
Open this example in the tool

Options API component with arrow functions

The single-parameter arrow in the filter loses its parentheses and trailing commas appear only in objects.

Input
<script>
export default{name:'OrderList',props:{orders:{type:Array,required:true}},computed:{total(){return this.orders.reduce((sum,o)=>sum+o.total,0)},paid(){return this.orders.filter(o=>o.status==='paid')}},methods:{remove(id){this.$emit('remove',id)}}}
</script>
<template><ul><li v-for="o in paid" :key="o.id">{{o.id}} - {{o.total.toFixed(2)}} <button @click="remove(o.id)">Remove</button></li></ul><p>Total: {{total}}</p></template>
Output
<script>
export default {
  name: "OrderList",
  props: { orders: { type: Array, required: true } },
  computed: {
    total() {
      return this.orders.reduce((sum, o) => sum + o.total, 0);
    },
    paid() {
      return this.orders.filter(o => o.status === "paid");
    },
  },
  methods: {
    remove(id) {
      this.$emit("remove", id);
    },
  },
};
</script>
<template>
  <ul>
    <li v-for="o in paid" :key="o.id">
      {{ o.id }} - {{ o.total.toFixed(2) }}
      <button @click="remove(o.id)">Remove</button>
    </li>
  </ul>
  <p>Total: {{ total }}</p>
</template>
Open this example in the tool

Form input with many attributes and SCSS

The long input tag is split one attribute per line and the nested SCSS rules are expanded.

Input
<template><form @submit.prevent="submit"><input v-model="form.email" type="email" placeholder="[email protected]" @keyup.enter="submit" :class="{invalid:errors.email,active:focused}"></form></template>
<style lang="scss">.form{padding:1rem;input{border:1px solid #ccc;&.invalid{border-color:#dc2626}}}</style>
Output
<template>
  <form @submit.prevent="submit">
    <input
      v-model="form.email"
      type="email"
      placeholder="[email protected]"
      @keyup.enter="submit"
      :class="{ invalid: errors.email, active: focused }"
    />
  </form>
</template>
<style lang="scss">
.form {
  padding: 1rem;
  input {
    border: 1px solid #ccc;
    &.invalid {
      border-color: #dc2626;
    }
  }
}
</style>
Open this example in the tool

Common errors and how to fix them

ErrorCauseFix
Unexpected closing tag "template". It may happen when the tag has already been closed by another tag.
Explained
A closing tag does not match the element that is open, typically because an inner element was left unclosed or closed in the wrong order.Go to the reported line and close the inner elements before closing their parent.
Cannot read properties of null (reading 'end')An attribute value is missing its closing quote, as in @click="count++>, so the parser lost track of where the tag ends.Close the quote on the attribute value. The message is cryptic, so search the template for an odd number of double quotes.
The <script> block comes out exactly as it went inThe script contains a JavaScript or TypeScript syntax error. Prettier skips blocks it cannot parse instead of failing the whole file.Copy the script into the TypeScript or JavaScript formatter to see the exact error, fix it, then format the component again.
A <template lang="pug"> block is not formattedPrettier has no Pug parser here, so Pug templates are passed through as written.Format Pug with a dedicated Pug tool; the script and style blocks of the same file are still formatted.

Frequently asked questions

Does it support <script setup> and TypeScript?

Yes. Script setup is formatted like any other script block, and lang=“ts” switches the parser to TypeScript so interfaces, generics and defineProps type arguments work.

Can I format Vue 2 components?

Yes. Options API components with export default and a template block format the same way as Composition API ones.

Why is my script not indented inside the <script> tag?

That is Prettier’s default for Vue files and matches most Vue projects. The block content starts at the left margin.

Are SCSS and Less style blocks formatted?

Yes. Style blocks with lang=“scss” or lang=“less” are formatted with the matching CSS parser, and plain CSS is the default.

Does it check Vue directives such as v-for?

No. Directive values are formatted as expressions, but Vue-specific mistakes such as v-for=“item on items” are not reported. Use eslint-plugin-vue for that.

Related tools