Files
2026-04-17 23:26:01 +00:00

6.1 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Single-File Component Structure, Styling, and Template Patterns MEDIUM Consistent SFC structure and styling choices improve maintainability, tooling support, and render performance best-practice
vue3
sfc
scoped-css
styles
build-tools
performance
template
v-html
v-for
computed
v-if
v-show

Single-File Component Structure, Styling, and Template Patterns

Impact: MEDIUM - Using SFCs with consistent structure and performant styling keeps components easier to maintain and avoids unnecessary render overhead.

Task List

  • Use .vue SFCs instead of separate .js/.ts and .css files for components
  • Colocate template, script, and styles in the same SFC by default
  • Use PascalCase for component names in templates and filenames
  • Prefer component-scoped styles
  • Prefer class selectors (not element selectors) in scoped CSS for performance
  • Access DOM / component refs with useTemplateRef() in Vue 3.5+
  • Use camelCase keys in :style bindings for consistency and IDE support
  • Use v-for and v-if correctly
  • Never use v-html with untrusted/user-provided content
  • Choose v-if vs v-show based on toggle frequency and initial render cost

Colocate template, script, and styles

BAD:

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>

Use PascalCase for component names

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>

Best practices for <style> block in SFCs

Prefer component-scoped styles

  • Use <style scoped> for styles that belong to a component.
  • Keep global CSS in a dedicated file (e.g. src/assets/main.css) for resets, typography, tokens, etc.
  • Use :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; }

Use class selectors in scoped CSS

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>

Access DOM / component refs with 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>

Use camelCase in :style bindings

BAD:

<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>

Use v-for and v-if correctly

Always provide a stable :key

  • Prefer primitive keys (string | number).
  • Avoid using objects as keys.

GOOD:

<li v-for="item in items" :key="item.id">
  <input v-model="item.text" />
</li>

Avoid v-if and v-for on the same element

It 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>

Never render untrusted HTML with v-html

BAD:

<template>
  <!-- DANGEROUS: untrusted input can inject scripts -->
  <article v-html="userProvidedContent"></article>
</template>

GOOD:

<script setup>
import { computed } from 'vue'
import DOMPurify from 'dompurify'

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"></article>
</template>

Choose v-if vs v-show by toggle behavior

BAD:

<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>