本文 AI 產出,尚未審核

Vue3:樣式與 CSS 管理 — scoped styles 完全指南


簡介

在單頁應用 (SPA) 中,Vue 組件往往會同時承載 HTML、JavaScript 與 CSS。若所有樣式都寫在全域樣式表,隨著專案規模成長,就會出現 樣式衝突維護成本飆升 等問題。Vue3 為了解決這個痛點,提供了 scoped 樣式的機制,讓每個 <style> 標籤只作用於其所在的組件,形成 樣式封裝

scoped 不僅能提升開發效率,還能減少因全域樣式覆寫所造成的 bug。對於從初學者晉升為中級開發者的你,掌握 scoped styles 是打造可維護、可擴充 UI 的關鍵一步。


核心概念

1. 為什麼需要 scoped?

  • 避免樣式污染:不同組件的 class 名稱可能相同,若不加限制,後載入的樣式會覆寫前面的樣式。
  • 提升可讀性:開發者只需要關注組件內部的樣式,無需在全域樣式檔中搜尋相關規則。
  • 支援熱重載:在開發環境下,修改 scoped 樣式會即時更新組件,不會影響其他頁面。

2. 基本語法

在 Vue 單檔元件 (*.vue) 中,只要在 <style> 標籤加上 scoped 屬性,即可啟用:

<template>
  <div class="card">
    <h2 class="title">Vue3 Scoped Demo</h2>
    <p class="content">這段文字只會受到本組件的樣式影響。</p>
  </div>
</template>

<script setup>
/* 這裡放組件邏輯 */
</script>

<style scoped>
.card {
  border: 1px solid #ddd;
  padding: 16px;
  border-radius: 8px;
}
.title {
  color: #42b983;
}
.content {
  font-size: 0.9rem;
  color: #555;
}
</style>

Vue 會在編譯階段為每個元素自動加上一個 唯一的屬性標記(例如 data-v-123abc),並把 CSS selector 改寫為 .card[data-v-123abc],從而實現樣式封裝。

3. 動態 class 與 inline style

即使使用 :class:style 動態綁定,scoped 樣式仍會正常作用,只要綁定的 class 名稱與 CSS 中的選擇器相同即可。

<template>
  <button :class="{ active: isActive }" @click="toggle">
    {{ isActive ? '已啟用' : '未啟用' }}
  </button>
</template>

<script setup>
import { ref } from 'vue'
const isActive = ref(false)
function toggle() { isActive.value = !isActive.value }
</script>

<style scoped>
button {
  background: #eee;
  border: none;
  padding: 8px 16px;
}
button.active {
  background: #42b983;
  color: #fff;
}
</style>

4. 深層選擇子組件 — ::v-deep / :deep()

預設情況下,scoped 樣式 不會影響子組件 的 DOM。若需要從父組件直接改寫子組件的樣式,可使用 Vue 提供的深層選擇子選擇器:

<template>
  <ChildComponent />
</template>

<style scoped>
/* 針對子組件內部的 .inner 進行樣式覆寫 */
::v-deep .inner {
  color: red;
}

/* Vue 3.3+ 推薦寫法 */
:deep(.inner) {
  font-weight: bold;
}
</style>

注意:深層選擇會破壞封裝,請謹慎使用,僅在確實需要時才採取。

5. 多個 <style> 標籤的組合

一個組件可以同時擁有 scoped全域 樣式,或是分別使用 CSS Modules

<template>
  <div :class="$style.box">使用 CSS Modules</div>
</template>

<script setup>
/* 無需額外程式碼 */
</script>

<style scoped>
/* 只在本組件內部生效 */
.box {
  border: 2px solid #42b983;
}
</style>

<style module>
/* 產生唯一的 class 名稱,透過 $style 取得 */
.box {
  padding: 12px;
  background: #f9f9f9;
}
</style>

程式碼範例

以下提供 5 個實務範例,從最基礎到較進階的使用情境,幫助你快速上手。

範例 1️⃣ 基本 scoped

<!-- File: BaseCard.vue -->
<template>
  <section class="card">
    <slot />
  </section>
</template>

<style scoped>
.card {
  box-shadow: 0 2px 8px rgba(0,0,0,.1);
  border-radius: 6px;
  padding: 20px;
  background: #fff;
}
</style>

說明:所有使用 <BaseCard> 的地方,都會自動套用這段樣式而不會影響到其他 .card 元素。


範例 2️⃣ 動態 class 結合 scoped

<!-- File: ToggleBtn.vue -->
<template>
  <button :class="{ on: active }" @click="active = !active">
    {{ active ? 'ON' : 'OFF' }}
  </button>
</template>

<script setup>
import { ref } from 'vue'
const active = ref(false)
</script>

<style scoped>
button {
  width: 80px;
  height: 36px;
  border: 1px solid #ccc;
  background: #fafafa;
  cursor: pointer;
}
button.on {
  background: #42b983;
  color: #fff;
}
</style>

說明:即使 on 是動態加上的,scoped 機制仍會正確匹配 button.on[data-v-xxxx]


範例 3️⃣ 深層樣式(:deep)

<!-- File: Parent.vue -->
<template>
  <Child />
</template>

<style scoped>
/* 想要改寫 Child 裡的 .inner */
:deep(.inner) {
  color: #ff5722;
  font-size: 1.2rem;
}
</style>
<!-- File: Child.vue -->
<template>
  <p class="inner">我是子組件的文字</p>
</template>

<style scoped>
.inner {
  color: #333;
}
</style>

說明:父層使用 :deep() 成功覆寫子層的文字顏色,示範了跨層級樣式的使用方式。


範例 4️⃣ 使用 CSS Modules

<!-- File: Card.module.vue -->
<template>
  <div :class="$style.wrapper">
    <h3 :class="$style.title">模組化卡片</h3>
    <slot />
  </div>
</template>

<style module>
.wrapper {
  border: 1px solid #e0e0e0;
  padding: 16px;
  border-radius: 4px;
}
.title {
  margin: 0 0 8px;
  color: #42b983;
}
</style>

說明$style.wrapper 會在編譯時被替換成類似 wrapper_1a2b3c 的唯一名稱,徹底避免衝突。


範例 5️⃣ 同時使用全域與 scoped

<!-- File: GlobalDemo.vue -->
<template>
  <section class="global-section">
    <p class="local">這段文字只在此組件內部受樣式控制。</p>
  </section>
</template>

<style>
/* 全域樣式:所有 .global-section 都會受到影響 */
.global-section {
  background: #f0f4f8;
  padding: 20px;
}
</style>

<style scoped>
.local {
  color: #42b983;
  font-weight: 600;
}
</style>

說明:全域樣式適用於跨組件的布局或主題色,scoped 樣式則只負責組件內部的細部呈現。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方案或最佳實踐
忘記加 scoped 樣式意外全域洩漏,導致其他組件被改寫 養成檢查:每個 .vue 檔的 <style> 必須明確標示 scoped(或使用 CSS Modules)
過度使用 :deep 破壞封裝,維護成本升高 僅在必要時 使用;若需要大量深層樣式,考慮將共用樣式抽離成全域樣式或 UI 套件
依賴 !important 讓樣式層級變得難以預測 避免:使用更具體的 selector 或 CSS Modules 取代
同一檔案內混用多個 scoped 標籤 產生冗餘的屬性標記,影響編譯效能 統一管理:盡量將所有 scoped 樣式放在同一個 <style scoped>
第三方 UI 庫的樣式被遮蔽 元件外觀不如預期 使用全域樣式或 :deep 針對第三方類名做微調,或在 vite.config.js 中設定 css.preprocessorOptions 讓其預先載入
樣式命名衝突(即使有 scoped 例如 ::v-deep .btn 與全域 .btn 同時存在 採用 BEM 或 CSS Modules,保持命名唯一性

最佳實踐小結

  1. 預設使用 scoped:除非有明確需求,所有組件樣式皆以 scoped 為主。
  2. 結合 CSS Modules:對於需要高度唯一性的 class,使用 <style module>
  3. 適度使用全域樣式:全域樣式適合佈局、字體、色系等「全站」規則。
  4. 保持樣式簡潔:每個組件只寫與該組件直接相關的 CSS,避免過度巢狀。
  5. 使用預處理器(如 SCSS)提升可讀性,並結合 @use@forward 共享變數。

實際應用場景

場景 為何使用 scoped 實作要點
企業內部儀表板 多個卡片、表格、圖表同時出現在同一頁面,樣式衝突風險高 每個圖表元件使用 scoped,共用配色與字體放在全域 CSS
可重用 UI 元件庫(如 Button、Modal) 元件會被不同專案多次引用,必須保證不受外部樣式影響 內部採用 scoped + CSS Modules,外部提供主題變數供全域覆寫
多語系或深色模式切換 主題樣式需要全局切換,單一組件仍保持自己的結構樣式 使用 scoped 控制結構,將顏色變數放在根元素 :rootdata-theme
第三方 UI 套件整合(如 Element Plus) 套件內部已自帶樣式,避免自家樣式覆寫 只在需要微調的地方使用 :deep,其餘保持原樣式不變
大型表單與驗證訊息 表單欄位與錯誤訊息常會有相同 class 名稱(如 .error 每個表單元件使用 scoped,錯誤樣式只在該表單內部生效

總結

  • scoped styles 是 Vue3 為了解決樣式污染、提升可維護性而設計的核心功能。透過自動加上的屬性標記,讓每個組件的 CSS 只在自己的範圍內生效
  • 只要在 <style> 標籤加上 scoped,或使用 CSS Modules,就能輕鬆實現樣式封裝。對於需要跨層級調整的情況,::v-deep / :deep() 提供了受控的深層選擇器。
  • 在實務開發中,避免過度使用 :deep!important,並結合 全域樣式預處理器命名規則,才能寫出既安全又易於維護的 UI。
  • 透過上述範例與最佳實踐,你可以在任何規模的 Vue3 專案中,快速建立 乾淨、可預測、可重用 的樣式系統。

把握 scoped 的力量,讓你的 Vue3 應用從「會跑」變成「好維護」!