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,保持命名唯一性 |
最佳實踐小結
- 預設使用
scoped:除非有明確需求,所有組件樣式皆以 scoped 為主。 - 結合 CSS Modules:對於需要高度唯一性的 class,使用
<style module>。 - 適度使用全域樣式:全域樣式適合佈局、字體、色系等「全站」規則。
- 保持樣式簡潔:每個組件只寫與該組件直接相關的 CSS,避免過度巢狀。
- 使用預處理器(如 SCSS)提升可讀性,並結合
@use、@forward共享變數。
實際應用場景
| 場景 | 為何使用 scoped | 實作要點 |
|---|---|---|
| 企業內部儀表板 | 多個卡片、表格、圖表同時出現在同一頁面,樣式衝突風險高 | 每個圖表元件使用 scoped,共用配色與字體放在全域 CSS |
| 可重用 UI 元件庫(如 Button、Modal) | 元件會被不同專案多次引用,必須保證不受外部樣式影響 | 內部採用 scoped + CSS Modules,外部提供主題變數供全域覆寫 |
| 多語系或深色模式切換 | 主題樣式需要全局切換,單一組件仍保持自己的結構樣式 | 使用 scoped 控制結構,將顏色變數放在根元素 :root 或 data-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 應用從「會跑」變成「好維護」!