Vue3 課程 – 樣式與 CSS 管理
主題:Tailwind CSS 整合
簡介
在 Vue 3 生態系中,樣式的管理往往是開發者面臨的最大挑戰之一。傳統的 CSS、SCSS 或是 CSS‑Modules 雖然功能完整,但在大型專案裡容易產生樣式衝突、重複定義以及維護成本高的問題。Tailwind CSS 以「utility‑first」的概念重新定義了樣式編寫方式,讓開發者透過原子化的 class 直接在模板中描述 UI,減少了樣式檔案的切換與命名衝突。
將 Tailwind 與 Vue 3 結合,不僅能夠保留 Vue 組件化的優勢,還能在 開發速度、可讀性 與 可維護性 上取得顯著提升。本篇文章將從環境建置、核心概念、實作範例、常見陷阱到最佳實踐,完整說明如何在 Vue 3 專案中順利整合 Tailwind CSS,讓你在專案中快速上手、即時產出美觀且一致的 UI。
核心概念
1. Utility‑First 與「原子化」的思維
Tailwind 的核心是 utility class,每個 class 只負責一件事(例如 p-4 代表「內距 1rem」)。透過把這些小工具組合起來,就能完成完整的介面,而不需要寫自訂的 CSS 規則。
優點
- 即時預覽:在瀏覽器中直接看到效果,省去切換檔案的時間。
- 避免命名衝突:所有 class 都是預先定義好的字串,不會因為命名不當產生衝突。
- 高度可組合:同一個 class 可以在不同組件中重複使用,形成設計系統。
2. 在 Vue 3 中使用 Tailwind
Vue 3 支援 單檔元件 (Single‑File Component, .vue),而 Tailwind 只需要在 PostCSS 流程中被載入即可。最常見的做法是:
- 安裝 Tailwind 相關套件
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest npx tailwindcss init -p # 產生 tailwind.config.js 與 postcss.config.js - 在
src/assets/tailwind.css中引入基礎指令@tailwind base; @tailwind components; @tailwind utilities; - 在
main.js(或main.ts)中全域匯入import { createApp } from 'vue' import App from './App.vue' import './assets/tailwind.css' // <-- 這行讓所有元件都能使用 Tailwind createApp(App).mount('#app')
小技巧:若想在特定元件內限定 Tailwind,亦可在該元件的
<style>區塊使用@import "./tailwind.css";,但全域匯入是最常見且最簡潔的方式。
3. 透過 @apply 建立自訂類別
有時候直接在模板中寫長串的 utility class 會影響可讀性,這時可以利用 Tailwind 的 @apply 指令,把常用組合抽成自訂類別:
/* src/assets/custom.css */
.btn-primary {
@apply bg-blue-600 text-white font-semibold py-2 px-4 rounded hover:bg-blue-700;
}
在 Vue 元件中:
<template>
<button class="btn-primary">送出</button>
</template>
<script setup>
/* 這裡不需要額外的程式碼 */
</script>
<style src="../assets/custom.css"></style>
4. 配合 Vue 3 的 :class 動態綁定
Tailwind 的 class 多為靜態字串,但在實作互動效果時,我們常需要根據狀態切換 class。Vue 的 動態綁定 完全相容:
<template>
<button
:class="isActive ? 'bg-green-500' : 'bg-gray-300'"
class="text-white font-bold py-2 px-4 rounded transition-colors duration-200"
@click="toggle"
>
{{ isActive ? '已啟用' : '未啟用' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(false)
function toggle() {
isActive.value = !isActive.value
}
</script>
技巧:將常見的狀態樣式抽成物件,讓
:class更易維護:const btnClass = computed(() => ({ 'bg-green-500': isActive.value, 'bg-gray-300': !isActive.value, }))
5. 配置 Purge(Tree‑shaking)以減少產出檔案
Tailwind 預設會產生超過 3 MB 的 CSS,若直接上線會拖慢載入速度。Vue CLI、Vite 或 Nuxt 都提供 purge 機制,只保留實際使用的 class:
// tailwind.config.js
module.exports = {
content: [
'./index.html',
'./src/**/*.vue',
'./src/**/*.js',
],
theme: {
extend: {},
},
plugins: [],
}
在 vite.config.js 中不需要額外設定,Vite 會自動讀取 content 欄位。
程式碼範例
以下提供 5 個實用範例,展示在 Vue 3 中運用 Tailwind 完成常見 UI 元件的寫法。每個範例皆附有註解,說明關鍵概念與最佳做法。
範例 1:響應式卡片 (Responsive Card)
<!-- src/components/ResponsiveCard.vue -->
<template>
<div class="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
<div class="md:flex">
<!-- 圖片 -->
<div class="md:flex-shrink-0">
<img class="h-48 w-full object-cover md:h-full md:w-48"
src="https://picsum.photos/200/300"
alt="隨機圖片">
</div>
<!-- 內容 -->
<div class="p-8">
<div class="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
範例卡片
</div>
<a href="#" class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">
使用 Tailwind 打造的卡片
</a>
<p class="mt-2 text-gray-500">
這裡可以放說明文字,利用 Tailwind 的排版工具快速調整行距與顏色。
</p>
</div>
</div>
</div>
</template>
<script setup>
/* 此卡片為純展示,無需額外腳本 */
</script>
說明:
md:前綴代表 中斷點(768px)以上才套用,實現桌面與手機的不同排版。max-w-sm與md:max-w-2xl結合,使卡片在小螢幕時寬度受限,放大螢幕時自動展開。
範例 2:表單驗證 UI(使用 v-model + 動態 class)
<!-- src/components/FormInput.vue -->
<template>
<div class="space-y-2">
<label :for="id" class="block text-sm font-medium text-gray-700">
{{ label }}
</label>
<input
:id="id"
v-model="modelValue"
:class="inputClass"
@blur="touched = true"
class="block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
:type="type"
:placeholder="placeholder"
/>
<p v-if="showError" class="text-sm text-red-600">
{{ errorMessage }}
</p>
</div>
</template>
<script setup>
import { ref, computed, defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: String,
label: String,
id: String,
type: { type: String, default: 'text' },
placeholder: String,
required: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const touched = ref(false)
const inputClass = computed(() => ({
'border-gray-300': !showError.value,
'border-red-500': showError.value,
}))
const showError = computed(() => props.required && touched.value && !props.modelValue)
const errorMessage = computed(() => {
if (!props.required) return ''
return !props.modelValue ? '此欄位為必填' : ''
})
// 讓 v-model 雙向綁定
watch(() => props.modelValue, (val) => {
emit('update:modelValue', val)
})
</script>
說明:
- 使用
:class動態切換border-gray-300與border-red-500,即時呈現驗證結果。watch讓子元件能回傳更新值,保持v-model的雙向綁定。
範例 3:自訂按鈕組件(結合 @apply)
/* src/assets/buttons.css */
.btn-primary {
@apply bg-indigo-600 text-white font-medium py-2 px-4 rounded hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500;
}
.btn-outline {
@apply border border-indigo-600 text-indigo-600 font-medium py-2 px-4 rounded hover:bg-indigo-50;
}
<!-- src/components/BaseButton.vue -->
<template>
<button
:class="variantClass"
@click="$emit('click')"
class="transition-colors duration-150"
>
<slot />
</button>
</template>
<script setup>
import { computed, defineProps, defineEmits } from 'vue'
const props = defineProps({
variant: { type: String, default: 'primary' }, // primary / outline
})
const emit = defineEmits(['click'])
const variantClass = computed(() => {
return props.variant === 'outline' ? 'btn-outline' : 'btn-primary'
})
</script>
<style src="../assets/buttons.css"></style>
說明:
- 透過
@apply把常用的 Tailwind 組合寫進 CSS,讓模板只保留btn-primary、btn-outline兩個語意化 class。variant屬性讓按鈕樣式可配置,維持元件的可重用性。
範例 4:使用 v-for 產生動態列表(配合 key 與 transition)
<!-- src/components/TodoList.vue -->
<template>
<div class="max-w-md mx-auto">
<h2 class="text-2xl font-bold mb-4">Todo List</h2>
<ul>
<transition-group name="list" tag="div">
<li
v-for="item in items"
:key="item.id"
class="flex items-center justify-between p-3 mb-2 bg-gray-100 rounded hover:bg-gray-200"
>
<span :class="{ 'line-through text-gray-500': item.done }">
{{ item.text }}
</span>
<div class="flex space-x-2">
<button @click="toggle(item.id)" class="text-sm text-indigo-600">
{{ item.done ? 'Undo' : 'Done' }}
</button>
<button @click="remove(item.id)" class="text-sm text-red-600">
Delete
</button>
</div>
</li>
</transition-group>
</ul>
<input
v-model="newTodo"
@keyup.enter="add"
class="mt-4 w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="輸入新任務"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: '學習 Vue 3', done: false },
{ id: 2, text: '整合 Tailwind', done: false },
])
const newTodo = ref('')
function add() {
if (!newTodo.value.trim()) return
items.value.push({
id: Date.now(),
text: newTodo.value,
done: false,
})
newTodo.value = ''
}
function toggle(id) {
const target = items.value.find(i => i.id === id)
if (target) target.done = !target.done
}
function remove(id) {
items.value = items.value.filter(i => i.id !== id)
}
</script>
<style>
/* transition-group 的簡易動畫 */
.list-enter-active,
.list-leave-active {
transition: all 0.2s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>
說明:
v-for搭配:key確保 Vue 正確追蹤每筆資料,避免更新時的 DOM 重排。transition-group結合 Tailwind 的排版與自訂 CSS,提供滑入滑出的動畫效果。
範例 5:Dark Mode 切換(利用 Tailwind 的 dark: 前綴)
<!-- src/components/DarkModeToggle.vue -->
<template>
<div class="flex items-center space-x-2">
<span class="text-sm">🌞 Light</span>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" v-model="isDark" class="sr-only peer" @change="toggleDark" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-indigo-500 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-indigo-600"></div>
</label>
<span class="text-sm">🌙 Dark</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const isDark = ref(false)
function toggleDark() {
document.documentElement.classList.toggle('dark', isDark.value)
}
// 初始載入時根據系統偏好設定
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDark.value = true
toggleDark()
}
</script>
說明:
- Tailwind 內建
dark:前綴,可在同一套樣式表中寫出深色與淺色的差異。- 透過在
document.documentElement加上darkclass,整個應用的樣式會自動切換。
常見陷阱與最佳實踐
| 常見問題 | 可能原因 | 解決方案 |
|---|---|---|
| 產出的 CSS 檔案過大 | 未正確設定 content(purge)路徑 |
確保 tailwind.config.js 中列出所有 .vue、.js、.html 檔案路徑,或使用 Vite 的自動偵測功能。 |
在 :class 動態綁定時,Tailwind 未產生相對應的 class |
Tailwind 的 purge 只能掃描 靜態字串,動態拼接會被忽略。 | 使用 safe list(safelist)或把可能的 class 列在 content 中的註解區塊,例如 /* bg-red-500 bg-green-500 */。 |
| Tailwind 樣式被全局 CSS 覆蓋 | 順序錯誤:自訂 CSS 在 Tailwind 後載入。 | 確保 @tailwind utilities; 在自訂 CSS 之前,或在自訂樣式使用 !important(盡量避免)。 |
| Dark Mode 切換後部分元件未更新 | 元件已在建立時就寫死 class,未使用 dark: 前綴。 |
所有需要變色的 class 必須加上 dark: 前綴,或使用 @apply 產生自訂 dark class。 |
| 開發時 Tailwind JIT 未即時編譯 | 使用舊版 Tailwind(pre‑2.0)或未啟用 JIT 模式。 | 在 tailwind.config.js 加入 mode: 'jit'(Tailwind 3.x 已預設 JIT),或升級至最新版本。 |
最佳實踐
- 語意化 class:即使 Tailwind 鼓勵直接使用 utility,仍可透過
@apply把常見組合抽成語意化的 class(如.btn-primary),提升可讀性。 - 分層管理:將 全域樣式(reset、typography)放在
base.css,元件樣式(@apply)放在components/,保持結構清晰。 - 使用
@layer:Tailwind 允許在自訂 CSS 中指定層級,確保自訂規則在正確的層次(base、components、utilities)被注入。@layer components { .card { @apply rounded-lg shadow-lg p-6 bg-white; } } - 保持原子化:除非真的需要複用,否則盡量直接在模板中寫 utility,避免過度抽象化導致樣式分散。
- 開發環境開啟 JIT:JIT 會在檔案變動時即時生成 class,提升開發效率與即時預覽體驗。
實際應用場景
1. 企業內部管理系統
在大型企業內部系統中,介面往往需要 統一的設計語言,而且開發速度非常重要。透過 Tailwind:
- 快速原型:設計師會提供設計稿,前端只需要對照設計稿把對應的 utility class 寫進模板,即可在 1–2 小時內完成頁面。
- 統一主題:在
tailwind.config.js中自訂顏色、字型、間距等,所有元件自動套用,避免手動調整。 - 暗黑模式:只要在根元素加上
darkclass,所有使用dark:前綴的樣式即時切換,省去大量的 CSS 覆寫工作。
2. SaaS 產品的多租戶 UI
SaaS 產品常需要根據不同客戶的品牌色調做客製化。Tailwind 的 主題化 能讓我們在 tailwind.config.js 中動態注入顏色變數,搭配 Vite 的環境變數即可實現:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: `var(--brand-primary)`,
secondary: `var(--brand-secondary)`,
},
},
},
}
在租戶載入時,透過 JavaScript 設置 --brand-primary、--brand-secondary 的值,即可在所有使用 text-primary、bg-primary 的地方自動套用客戶專屬色彩。
3. 行動端 App(使用 Vue 3 + Vite + Capacitor)
Tailwind 的 響應式斷點 (sm:, md:, lg:) 與 原子化排版 讓行動端 UI 開發變得非常高效。配合 Vue 3 的 Composition API,可以在同一個元件內完成:
- 表單驗證(範例 2)
- 即時切換暗黑模式(範例 5)
- 滑動動畫(配合
transition-group)
最終產出的 CSS 經過 purge 後,僅保留實際使用的 class,載入體積小於 30KB,符合行動端的效能需求。
總結
Tailwind CSS 與 Vue 3 的結合,是現代前端開發 提升開發效率、降低維護成本 的最佳實踐之一。透過本文的五個實作範例,你可以快速掌握以下關鍵點:
- 環境建置:使用
npm、tailwind.config.js、postcss.config.js完成全域匯入。 - Utility‑First 思維:善用原子化 class,讓 UI 直接在模板中描述。
- 動態樣式:結合 Vue 的
:class、computed、watch,實作互動式 UI。 - 自訂與抽象:使用
@apply、@layer把常用樣式抽成語意化 class,保持程式碼可讀性。 - 最佳化:設定 purge / safelist、使用 JIT、適當的檔案結構,確保產出檔案精簡。
在實務專案中,無論是 企業內部系統、多租戶 SaaS,還是 行動端 App,Tailwind 都能提供一致且可擴充的樣式解決方案。只要遵循本文的 最佳實踐,配合 Vue 3 的組件化與 Composition API,你將能以最少的 CSS 代碼,快速打造出美觀、響應式、支援暗黑模式的現代化介面。
祝你在 Vue 3 + Tailwind 的旅程中玩得開心,寫出更乾淨、更高效的前端程式碼! 🚀