本文 AI 產出,尚未審核

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 流程中被載入即可。最常見的做法是:

  1. 安裝 Tailwind 相關套件
    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
    npx tailwindcss init -p   # 產生 tailwind.config.js 與 postcss.config.js
    
  2. src/assets/tailwind.css 中引入基礎指令
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  3. 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-smmd: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-300border-red-5​00,即時呈現驗證結果。
  • 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-primarybtn-outline 兩個語意化 class。
  • variant 屬性讓按鈕樣式可配置,維持元件的可重用性。

範例 4:使用 v-for 產生動態列表(配合 keytransition

<!-- 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 加上 dark class,整個應用的樣式會自動切換。

常見陷阱與最佳實踐

常見問題 可能原因 解決方案
產出的 CSS 檔案過大 未正確設定 content(purge)路徑 確保 tailwind.config.js 中列出所有 .vue.js.html 檔案路徑,或使用 Vite 的自動偵測功能。
:class 動態綁定時,Tailwind 未產生相對應的 class Tailwind 的 purge 只能掃描 靜態字串,動態拼接會被忽略。 使用 safe listsafelist)或把可能的 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),或升級至最新版本。

最佳實踐

  1. 語意化 class:即使 Tailwind 鼓勵直接使用 utility,仍可透過 @apply 把常見組合抽成語意化的 class(如 .btn-primary),提升可讀性。
  2. 分層管理:將 全域樣式(reset、typography)放在 base.css元件樣式@apply)放在 components/,保持結構清晰。
  3. 使用 @layer:Tailwind 允許在自訂 CSS 中指定層級,確保自訂規則在正確的層次(base、components、utilities)被注入。
    @layer components {
      .card { @apply rounded-lg shadow-lg p-6 bg-white; }
    }
    
  4. 保持原子化:除非真的需要複用,否則盡量直接在模板中寫 utility,避免過度抽象化導致樣式分散。
  5. 開發環境開啟 JIT:JIT 會在檔案變動時即時生成 class,提升開發效率與即時預覽體驗。

實際應用場景

1. 企業內部管理系統

在大型企業內部系統中,介面往往需要 統一的設計語言,而且開發速度非常重要。透過 Tailwind:

  • 快速原型:設計師會提供設計稿,前端只需要對照設計稿把對應的 utility class 寫進模板,即可在 1–2 小時內完成頁面。
  • 統一主題:在 tailwind.config.js 中自訂顏色、字型、間距等,所有元件自動套用,避免手動調整。
  • 暗黑模式:只要在根元素加上 dark class,所有使用 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-primarybg-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 的結合,是現代前端開發 提升開發效率、降低維護成本 的最佳實踐之一。透過本文的五個實作範例,你可以快速掌握以下關鍵點:

  1. 環境建置:使用 npmtailwind.config.jspostcss.config.js 完成全域匯入。
  2. Utility‑First 思維:善用原子化 class,讓 UI 直接在模板中描述。
  3. 動態樣式:結合 Vue 的 :classcomputedwatch,實作互動式 UI。
  4. 自訂與抽象:使用 @apply@layer 把常用樣式抽成語意化 class,保持程式碼可讀性。
  5. 最佳化:設定 purge / safelist、使用 JIT、適當的檔案結構,確保產出檔案精簡。

在實務專案中,無論是 企業內部系統多租戶 SaaS,還是 行動端 App,Tailwind 都能提供一致且可擴充的樣式解決方案。只要遵循本文的 最佳實踐,配合 Vue 3 的組件化與 Composition API,你將能以最少的 CSS 代碼,快速打造出美觀、響應式、支援暗黑模式的現代化介面。

祝你在 Vue 3 + Tailwind 的旅程中玩得開心,寫出更乾淨、更高效的前端程式碼! 🚀