Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Enforce Vue 3 best practices—Composition API, script setup, TypeScript, component boundaries, and reactivity patterns
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/sfc.md
1---2title: Single-File Component Structure, Styling, and Template Patterns3impact: MEDIUM4impactDescription: Consistent SFC structure and styling choices improve maintainability, tooling support, and render performance5type: best-practice6tags: [vue3, sfc, scoped-css, styles, build-tools, performance, template, v-html, v-for, computed, v-if, v-show]7---89# Single-File Component Structure, Styling, and Template Patterns1011**Impact: MEDIUM** - Using SFCs with consistent structure and performant styling keeps components easier to maintain and avoids unnecessary render overhead.1213## Task List1415- Use `.vue` SFCs instead of separate `.js`/`.ts` and `.css` files for components16- Colocate template, script, and styles in the same SFC by default17- Use PascalCase for component names in templates and filenames18- Prefer component-scoped styles19- Prefer class selectors (not element selectors) in scoped CSS for performance20- Access DOM / component refs with `useTemplateRef()` in Vue 3.5+21- Use camelCase keys in `:style` bindings for consistency and IDE support22- Use `v-for` and `v-if` correctly23- Never use `v-html` with untrusted/user-provided content24- Choose `v-if` vs `v-show` based on toggle frequency and initial render cost2526## Colocate template, script, and styles2728**BAD:**29```30components/31├── UserCard.vue32├── UserCard.js33└── UserCard.css34```3536**GOOD:**37```vue38<!-- components/UserCard.vue -->39<script setup>40import { computed } from 'vue'4142const props = defineProps({43user: { type: Object, required: true }44})4546const displayName = computed(() =>47`${props.user.firstName} ${props.user.lastName}`48)49</script>5051<template>52<div class="user-card">53<h3 class="name">{{ displayName }}</h3>54</div>55</template>5657<style scoped>58.user-card {59padding: 1rem;60}6162.name {63margin: 0;64}65</style>66```6768## Use PascalCase for component names6970**BAD:**71```vue72<script setup>73import userProfile from './user-profile.vue'74</script>7576<template>77<user-profile :user="currentUser" />78</template>79```8081**GOOD:**82```vue83<script setup>84import UserProfile from './UserProfile.vue'85</script>8687<template>88<UserProfile :user="currentUser" />89</template>90```9192## Best practices for `<style>` block in SFCs9394### Prefer component-scoped styles9596- Use `<style scoped>` for styles that belong to a component.97- Keep **global CSS** in a dedicated file (e.g. `src/assets/main.css`) for resets, typography, tokens, etc.98- Use `:deep()` sparingly (edge cases only).99100**BAD:**101102```vue103<style>104/* ❌ leaks everywhere */105button { border-radius: 999px; }106</style>107```108109**GOOD:**110111```vue112<style scoped>113.button { border-radius: 999px; }114</style>115```116117**GOOD:**118119```css120/* src/assets/main.css */121/* ✅ resets, tokens, typography, app-wide rules */122:root { --radius: 999px; }123```124125### Use class selectors in scoped CSS126127**BAD:**128```vue129<template>130<article>131<h1>{{ title }}</h1>132<p>{{ subtitle }}</p>133</article>134</template>135136<style scoped>137article { max-width: 800px; }138h1 { font-size: 2rem; }139p { line-height: 1.6; }140</style>141```142143**GOOD:**144```vue145<template>146<article class="article">147<h1 class="article-title">{{ title }}</h1>148<p class="article-subtitle">{{ subtitle }}</p>149</article>150</template>151152<style scoped>153.article { max-width: 800px; }154.article-title { font-size: 2rem; }155.article-subtitle { line-height: 1.6; }156</style>157```158159## Access DOM / component refs with `useTemplateRef()`160161For Vue 3.5+: use `useTemplateRef()` to access template refs.162163```vue164<script setup lang="ts">165import { onMounted, useTemplateRef } from 'vue'166167const inputRef = useTemplateRef<HTMLInputElement>('input')168169onMounted(() => {170inputRef.value?.focus()171})172</script>173174<template>175<input ref="input" />176</template>177```178179## Use camelCase in `:style` bindings180181**BAD:**182```vue183<template>184<div :style="{ 'font-size': fontSize + 'px', 'background-color': bg }">185Content186</div>187</template>188```189190**GOOD:**191```vue192<template>193<div :style="{ fontSize: fontSize + 'px', backgroundColor: bg }">194Content195</div>196</template>197```198199## Use `v-for` and `v-if` correctly200201### Always provide a stable `:key`202203- Prefer primitive keys (`string | number`).204- Avoid using objects as keys.205206**GOOD:**207208```vue209<li v-for="item in items" :key="item.id">210<input v-model="item.text" />211</li>212```213214### Avoid `v-if` and `v-for` on the same element215216It leads to unclear intent and unnecessary work.217([Reference](https://vuejs.org/guide/essentials/list.html#v-for-with-v-if))218219**To filter items**220**BAD:**221222```vue223<li v-for="user in users" v-if="user.active" :key="user.id">224{{ user.name }}225</li>226```227228**GOOD:**229230```vue231<script setup lang="ts">232import { computed } from 'vue'233234const activeUsers = computed(() => users.value.filter(u => u.active))235</script>236237<template>238<li v-for="user in activeUsers" :key="user.id">239{{ user.name }}240</li>241</template>242```243244**To conditionally show/hide the entire list**245**GOOD:**246247```vue248<ul v-if="shouldShowUsers">249<li v-for="user in users" :key="user.id">250{{ user.name }}251</li>252</ul>253```254255## Never render untrusted HTML with `v-html`256257**BAD:**258```vue259<template>260<!-- DANGEROUS: untrusted input can inject scripts -->261<article v-html="userProvidedContent"></article>262</template>263```264265**GOOD:**266```vue267<script setup>268import { computed } from 'vue'269import DOMPurify from 'dompurify'270271const props = defineProps<{272trustedHtml?: string273plainText: string274}>()275276const safeHtml = computed(() => DOMPurify.sanitize(props.trustedHtml ?? ''))277</script>278279<template>280<!-- Preferred: escaped interpolation -->281<p>{{ props.plainText }}</p>282283<!-- Only for trusted/sanitized HTML -->284<article v-html="safeHtml"></article>285</template>286```287288## Choose `v-if` vs `v-show` by toggle behavior289290**BAD:**291```vue292<template>293<!-- Frequent toggles with v-if cause repeated mount/unmount -->294<ComplexPanel v-if="isPanelOpen" />295296<!-- Rarely shown content with v-show pays initial render cost -->297<AdminPanel v-show="isAdmin" />298</template>299```300301**GOOD:**302```vue303<template>304<!-- Frequent toggles: keep in DOM, toggle display -->305<ComplexPanel v-show="isPanelOpen" />306307<!-- Rare condition: lazy render only when true -->308<AdminPanel v-if="isAdmin" />309</template>310```311