.agents/skills/vue-best-practices/references/sfc.md
Impact: MEDIUM - Using SFCs with consistent structure and performant styling keeps components easier to maintain and avoids unnecessary render overhead.
.vue SFCs instead of separate .js/.ts and .css files for componentsuseTemplateRef() in Vue 3.5+:style bindings for consistency and IDE supportv-for and v-if correctlyv-html with untrusted/user-provided contentv-if vs v-show based on toggle frequency and initial render costBAD:
components/
├── UserCard.vue
├── UserCard.js
└── UserCard.css
GOOD:
<!-- components/UserCard.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps({
user: { type: Object, required: true }
})
const displayName = computed(() =>
`${props.user.firstName} ${props.user.lastName}`
)
</script>
<template>
<div class="user-card">
<h3 class="name">
{{ displayName }}
</h3>
</div>
</template>
<style scoped>
.user-card {
padding: 1rem;
}
.name {
margin: 0;
}
</style>
BAD:
<script setup>
import userProfile from './user-profile.vue'
</script>
<template>
<user-profile :user="currentUser" />
</template>
GOOD:
<script setup>
import UserProfile from './UserProfile.vue'
</script>
<template>
<UserProfile :user="currentUser" />
</template>
<style> block in SFCs<style scoped> for styles that belong to a component.src/assets/main.css) for resets, typography, tokens, etc.:deep() sparingly (edge cases only).BAD:
<style>
/* ❌ leaks everywhere */
button { border-radius: 999px; }
</style>
GOOD:
<style scoped>
.button { border-radius: 999px; }
</style>
GOOD:
/* src/assets/main.css */
/* ✅ resets, tokens, typography, app-wide rules */
:root { --radius: 999px; }
BAD:
<template>
<article>
<h1>{{ title }}</h1>
<p>{{ subtitle }}</p>
</article>
</template>
<style scoped>
article { max-width: 800px; }
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
GOOD:
<template>
<article class="article">
<h1 class="article-title">
{{ title }}
</h1>
<p class="article-subtitle">
{{ subtitle }}
</p>
</article>
</template>
<style scoped>
.article { max-width: 800px; }
.article-title { font-size: 2rem; }
.article-subtitle { line-height: 1.6; }
</style>
useTemplateRef()For Vue 3.5+: use useTemplateRef() to access template refs.
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="input">
</template>
:style bindingsBAD:
<template>
<div :style="{ 'font-size': `${fontSize}px`, 'background-color': bg }">
Content
</div>
</template>
GOOD:
<template>
<div :style="{ fontSize: `${fontSize}px`, backgroundColor: bg }">
Content
</div>
</template>
v-for and v-if correctly:keystring | number).GOOD:
<li v-for="item in items" :key="item.id">
<input v-model="item.text" />
</li>
v-if and v-for on the same elementIt leads to unclear intent and unnecessary work. (Reference)
To filter items BAD:
<li v-for="user in users" v-if="user.active" :key="user.id">
{{ user.name }}
</li>
GOOD:
<script setup lang="ts">
import { computed } from 'vue'
const activeUsers = computed(() => users.value.filter(u => u.active))
</script>
<template>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
To conditionally show/hide the entire list GOOD:
<ul v-if="shouldShowUsers">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
v-htmlBAD:
<template>
<!-- DANGEROUS: untrusted input can inject scripts -->
<article v-html="userProvidedContent" />
</template>
GOOD:
<script setup>
import DOMPurify from 'dompurify'
import { computed } from 'vue'
const props = defineProps<{
trustedHtml?: string
plainText: string
}>()
const safeHtml = computed(() => DOMPurify.sanitize(props.trustedHtml ?? ''))
</script>
<template>
<!-- Preferred: escaped interpolation -->
<p>{{ props.plainText }}</p>
<!-- Only for trusted/sanitized HTML -->
<article v-html="safeHtml" />
</template>
v-if vs v-show by toggle behaviorBAD:
<template>
<!-- Frequent toggles with v-if cause repeated mount/unmount -->
<ComplexPanel v-if="isPanelOpen" />
<!-- Rarely shown content with v-show pays initial render cost -->
<AdminPanel v-show="isAdmin" />
</template>
GOOD:
<template>
<!-- Frequent toggles: keep in DOM, toggle display -->
<ComplexPanel v-show="isPanelOpen" />
<!-- Rare condition: lazy render only when true -->
<AdminPanel v-if="isAdmin" />
</template>