案例:儀表板 — 打造後台 Dashboard 版面 | TailwindCSS 完整教學
前兩篇我們做了講究「轉換」的行銷 Landing Page 與講究「閱讀」的內容型部落格,這一篇要挑戰版面最複雜、也最考驗 flex 與 grid 綜合功力的一種頁面——管理後台 Dashboard。我們會用 TailwindCSS v4 逐段打造一個可收合的固定側邊欄、sticky 頂部列、一整排 KPI 統計卡片、可捲動的資料表格,以及圖表版位,並認真處理行動版側欄滑入、表格 RWD 與暗色模式。做完你會得到一個能直接放後台功能上去的完整儀表板骨架。
前言
如果說 Landing Page 練的是「氣勢」、部落格練的是「閱讀」,那後台儀表板練的就是「資訊密度」。這是三種版面裡結構最硬的一種:畫面被切成側邊導覽、頂部工具列、內容區三大塊,內容區裡又塞著統計卡、圖表、表格,每一塊都有自己的捲動、對齊與響應式規則。它是 flex 與 grid 的綜合考題,也是每個做過管理系統的前端都逃不掉的日常。
這是本系列實戰案例的第三課,也是最後一堂。上一篇《案例:部落格》我們把重點放在 prose 與內容排版,主體是單純的三欄長文;這一篇的挑戰完全不同——後台版面是「固定外殼 + 可捲動內容」的組合。側邊欄要釘死、頂部列要黏住、只有中間主內容區跟著捲動,這種「框架不動、內容流動」的結構,正是所有 IDE、後台、Email 客戶端的共通骨架。學會它,你就掌握了應用程式級版面的基本功。
為什麼儀表板值得單獨做一課?因為它把前面學過的東西一次全用上,還逼你面對兩個新難題。第一是固定外殼的佈局:如何用 flex h-screen 讓側邊欄與主內容並排、各自處理自己的捲動,而不是整頁一起捲。第二是後台特有的響應式:桌機側邊欄常駐、手機要收成抽屜式滑入,以及資料表格在小螢幕怎麼不爆版。這兩題在行銷頁和部落格都不太會遇到,卻是後台的核心。
成品是一個單頁儀表板。左側是固定的側邊欄,有 LOGO、分組導覽選單、底部系統狀態與使用者資訊,手機上可收合成抽屜。頂部是一條 sticky 的工具列,放 hamburger、搜尋、通知與使用者選單。主內容區由上而下是:一排響應式的 KPI 統計卡片、一塊圖表版位,以及一張帶標題、搜尋、狀態徽章與分頁的資料表格。整頁完全響應式並支援暗色模式。
本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。
本篇你將學到:
- 固定外殼佈局:用
flex h-screen overflow-hidden搭min-w-0做出「側邊欄不動、主內容捲動」的應用程式骨架 - 收合式側邊欄:同一份 markup 如何在桌機常駐、手機用
translate-x抽屜式滑入,含遮罩與動畫 - 逐段實作:側邊欄 → 頂部列 → KPI 卡片 grid → 圖表佔位 → 資料表格,每段一個可執行區塊
- 後台的坑:側欄響應式失控、表格爆版、資訊密度過高等常見錯誤與正確做法
版面拆解
動手前,先把儀表板在腦中拆塊。它跟前兩篇最大的不同是:整頁不捲動,只有主內容區捲動。所以最外層不是我們熟悉的 max-w-7xl mx-auto,而是一個佔滿整個視窗、內部再切割的固定外殼。
┌──────────────┬──────────────────────────────────────┐
│ │ ≡ 搜尋… 🔔 [Avatar] ▾ │ ← Topbar (sticky)
│ LOGO ├──────────────────────────────────────┤
│ │ ┌──────┐┌──────┐┌──────┐┌──────┐ │
│ ▸ 總覽 │ │總收入 ││訂單數││新用戶││轉換率│ │ ← KPI grid
│ 分析 │ │$48.2k││ 328 ││ +42 ││3.24%│ │
│ 訂單 │ └──────┘└──────┘└──────┘└──────┘ │
│ 產品 │ ┌────────────────────┐┌───────────┐ │
│ 設定 │ │ 營收趨勢 (圖表) ││ 來源占比 │ │ ← 圖表版位
│ │ └────────────────────┘└───────────┘ │
│ ● 系統正常 │ ┌──────────────────────────────────┐ │
│ [Avatar]張三 │ │ 最近訂單 (資料表格 + 分頁) │ │ ← Data table
└──────────────┴──────────────────────────────────────┘
↑ 側邊欄固定 ↑ 只有這塊 overflow-y-auto 捲動
這個外殼是整篇的地基,它的佈局邏輯值得先講清楚:
- 最外層
flex h-screen overflow-hidden:讓側邊欄與主內容水平並排、高度都吃滿整個視窗,並鎖住整頁捲動——捲動交給內部區塊各自處理。 - 側邊欄固定寬
w-64 shrink-0:給它一個固定寬度且不准被壓縮(shrink-0),這樣不管主內容多寬,側欄永遠是 16rem。 - 主內容區
flex-1 min-w-0:用flex-1吃掉側欄以外的所有寬度;而min-w-0是最容易被忽略卻最關鍵的一條——沒有它,內部的寬表格會把整個 flex item 撐爆、擠壞側欄。 - 主內容再切上下兩層:頂部列
sticky top-0黏住,下方main用flex-1 overflow-y-auto成為唯一的捲動區。 - KPI 卡片用
grid:grid-cols-1 sm:grid-cols-2 xl:grid-cols-4,手機一欄、平板兩欄、大螢幕四欄。
心裡有了這張地圖——外殼用 flex 並排、側欄固定、主內容 min-w-0 且獨立捲動、卡片用 grid 換欄——接下來逐段填肉。
逐段實作
以下每段都是可獨立執行的 HTML 片段,搭配一個掛好 Tailwind v4 的頁面即可。我們由外而內:先搭好外殼與側邊欄,再做頂部列,接著填入 KPI 卡片、圖表版位,最後是資料表格。
整體外殼:固定側邊欄 + 可捲動主內容
先把地基打好。這段是整個儀表板的骨架:flex h-screen 讓側欄與主內容並排,側欄用 fixed + translate-x 準備好手機抽屜式收合,桌機再用 lg: 前綴切回常駐。
<!-- 儀表板外殼:flex 並排,鎖住整頁捲動 -->
<div class="flex h-screen overflow-hidden bg-gray-50 dark:bg-gray-950">
<!-- 側邊欄:桌機常駐,手機預設藏在畫面外(-translate-x-full) -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 flex w-64 shrink-0 flex-col
bg-white dark:bg-gray-900
border-r border-gray-200 dark:border-gray-800
-translate-x-full transition-transform duration-300 ease-in-out
lg:relative lg:translate-x-0">
<!-- 側邊欄內容(下一段實作) -->
</aside>
<!-- 手機側欄開啟時的半透明遮罩 -->
<div id="overlay"
class="fixed inset-0 z-40 hidden bg-black/50 backdrop-blur-sm lg:hidden"
aria-hidden="true"></div>
<!-- 主內容區:吃掉剩餘寬度,min-w-0 防止表格撐爆 -->
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
<!-- 頂部列(sticky) -->
<header class="sticky top-0 z-30 flex h-16 shrink-0 items-center gap-4 px-4 lg:px-6
bg-white/80 dark:bg-gray-900/80 backdrop-blur
border-b border-gray-200 dark:border-gray-800">
<!-- 頂部列內容(下一段實作) -->
</header>
<!-- 唯一的捲動區 -->
<main class="flex-1 overflow-y-auto p-4 lg:p-6">
<!-- KPI 卡片、圖表、表格都放這裡 -->
</main>
</div>
</div>
這段有三個地基級的重點。第一是 flex h-screen overflow-hidden:它讓整個版面像一個「不會整頁捲動的應用程式視窗」,側欄與主內容水平並排、各自滿版高。第二是側邊欄那組 fixed ... -translate-x-full ... lg:relative lg:translate-x-0:桌機(lg 以上)側欄是 relative、正常佔一欄且 translate-x-0 顯示;手機則是 fixed 定位並用 -translate-x-full 整個平移出畫面藏起來,等 hamburger 觸發才滑入。第三是主內容那個 min-w-0——這是後台版面最惡名昭彰的坑:flex item 預設 min-width: auto,當裡面有寬表格時它會拒絕縮小、把側欄擠爆;加上 min-w-0 才允許它正常收縮、讓內部的 overflow-x-auto 生效。
側邊欄:LOGO、分組導覽與底部狀態
外殼好了,來填側邊欄的內容。它本身也是一個垂直 flex:頂部 LOGO 固定高度、中間導覽選單 flex-1 吃剩餘空間並可捲動、底部釘著系統狀態與使用者資訊。導覽項目用 group 讓 hover 時圖示與文字一起變色,當前頁面用品牌色底標示 active 狀態。
<!-- 側邊欄內部:垂直 flex,頭固定/身捲動/尾固定 -->
<aside class="flex h-full w-64 flex-col bg-white dark:bg-gray-900
border-r border-gray-200 dark:border-gray-800">
<!-- LOGO 區(固定高度,對齊頂部列) -->
<div class="flex h-16 shrink-0 items-center gap-3 px-6
border-b border-gray-200 dark:border-gray-800">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-cyan-500">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"/>
</svg>
</div>
<span class="text-lg font-bold text-gray-900 dark:text-white">AdminKit</span>
</div>
<!-- 導覽選單(可捲動) -->
<nav class="flex-1 overflow-y-auto px-3 py-4" aria-label="主要導覽">
<p class="mb-2 px-3 text-xs font-semibold uppercase tracking-wider
text-gray-400 dark:text-gray-500">主選單</p>
<ul class="space-y-1">
<!-- 當前頁面:active 狀態,品牌色底 -->
<li>
<a href="#" aria-current="page"
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium
bg-cyan-50 text-cyan-700 dark:bg-cyan-900/20 dark:text-cyan-400">
<svg class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
</svg>
<span>總覽</span>
</a>
</li>
<!-- 一般項目:hover 才變色,用 group 連動圖示 -->
<li>
<a href="#"
class="group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium
text-gray-600 dark:text-gray-400
hover:bg-gray-100 hover:text-gray-900
dark:hover:bg-gray-800 dark:hover:text-white transition-colors">
<svg class="h-5 w-5 shrink-0 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14"/>
</svg>
<span>分析報告</span>
</a>
</li>
<li>
<a href="#"
class="group flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium
text-gray-600 dark:text-gray-400
hover:bg-gray-100 hover:text-gray-900
dark:hover:bg-gray-800 dark:hover:text-white transition-colors">
<svg class="h-5 w-5 shrink-0 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/>
</svg>
<span class="flex-1">訂單管理</span>
<span class="rounded-full bg-cyan-100 px-1.5 py-0.5 text-xs font-medium
text-cyan-600 dark:bg-cyan-900 dark:text-cyan-300">12</span>
</a>
</li>
</ul>
</nav>
<!-- 底部:系統狀態 + 使用者(固定) -->
<div class="shrink-0 border-t border-gray-200 p-4 dark:border-gray-800">
<div class="mb-2 flex items-center gap-2 rounded-lg bg-green-50 px-3 py-2 dark:bg-green-900/20">
<span class="h-2 w-2 animate-pulse rounded-full bg-green-500"></span>
<span class="text-xs font-medium text-green-700 dark:text-green-400">所有系統正常</span>
</div>
<div class="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2
hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full
bg-cyan-500 text-sm font-bold text-white">張</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">張三</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">admin@company.com</p>
</div>
</div>
</div>
</aside>
側邊欄的靈魂是那個垂直 flex flex-col 三段式結構:LOGO 與底部都是 shrink-0(固定高度、不被壓縮),中間的 <nav> 用 flex-1 overflow-y-auto 吃掉所有剩餘高度並在選單過長時自己捲動。這樣不管導覽項目有幾個,系統狀態與使用者資訊永遠釘在側欄最底部。另外兩個細節:active 項目用 bg-cyan-50 text-cyan-700 品牌色底做出「你在這頁」的視覺,一般項目則靠 group 讓整個連結 hover 時,內部原本灰色的 SVG 圖示用 group-hover:text-gray-600 一起變色——這是後台導覽最常用的互動手法。
頂部列:hamburger、搜尋與使用者選單
頂部列是連接側邊欄與主內容的橋樑。它最重要的功能是那顆 hamburger 按鈕(只在手機顯示,lg:hidden),點下去切換側欄的滑入。中間用 flex-1 撐開的搜尋框把左右兩側推開,右側放通知鈴鐺與使用者頭像。
<!-- 頂部列:sticky 已在外殼設定,這裡填內容 -->
<header class="sticky top-0 z-30 flex h-16 shrink-0 items-center gap-4 px-4 lg:px-6
bg-white/80 dark:bg-gray-900/80 backdrop-blur
border-b border-gray-200 dark:border-gray-800">
<!-- hamburger:只在手機顯示,觸發側欄滑入 -->
<button id="toggle" aria-label="開啟選單"
class="rounded-lg p-2 text-gray-500 hover:bg-gray-100
dark:text-gray-400 dark:hover:bg-gray-800 lg:hidden transition-colors">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- 搜尋框:flex-1 撐開,把右側推到底 -->
<div class="relative hidden max-w-md flex-1 sm:block">
<svg class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="search" placeholder="搜尋…"
class="w-full rounded-lg border border-gray-200 bg-gray-50 py-2 pl-9 pr-4 text-sm
text-gray-900 placeholder:text-gray-400
focus:border-transparent focus:outline-none focus:ring-2 focus:ring-cyan-500
dark:border-gray-700 dark:bg-gray-800 dark:text-white" />
</div>
<!-- 右側工具:用 ml-auto 推到最右 -->
<div class="ml-auto flex items-center gap-2">
<!-- 通知鈴鐺(帶紅點) -->
<button aria-label="通知"
class="relative rounded-lg p-2 text-gray-500 hover:bg-gray-100
dark:text-gray-400 dark:hover:bg-gray-800 transition-colors">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 00-4-5.7V5a2 2 0 10-4 0v.3A6 6 0 006 11v3.2c0 .5-.2 1-.6 1.4L4 17h5m6 0v1a3 3 0 11-6 0v-1"/>
</svg>
<span class="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white dark:ring-gray-900"></span>
</button>
<!-- 使用者頭像 -->
<button class="flex items-center gap-2 rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-cyan-500 text-sm font-bold text-white">張</div>
<svg class="hidden h-4 w-4 text-gray-400 sm:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
</div>
</header>
頂部列的排版全靠 flex 的三個小技巧。第一是搜尋框的 flex-1:它會吃掉中間所有空白,自然把 hamburger 推到最左、工具列推到最右。第二是右側工具的 ml-auto——即使手機上搜尋框被 hidden 藏起來(沒了 flex-1 撐開),ml-auto 仍能保證右側那組按鈕貼齊最右邊,版面不會塌。第三是 bg-white/80 backdrop-blur:半透明白底加毛玻璃,讓頂部列 sticky 黏住時,底下捲動的內容會透出一點朦朧感,是現代後台常見的質感細節。至於那顆 hamburger,它的 lg:hidden 確保只在需要收合側欄的小螢幕出現,桌機側欄常駐時它就自動消失。
KPI 統計卡片:responsive grid
進到主內容區,第一排是儀表板的門面——KPI 統計卡片。每張卡展示一個指標的名稱、數值、以及跟上期相比的趨勢(用綠色向上或紅色向下箭頭)。外層用 grid 做響應式換欄,是這段的主角。
<!-- KPI 卡片:手機一欄 / 平板兩欄 / 大螢幕四欄 -->
<div class="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<!-- 卡片:正向趨勢(綠) -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900
hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">本月總收入</p>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">$48,250</p>
</div>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
bg-cyan-50 dark:bg-cyan-900/20">
<svg class="h-5 w-5 text-cyan-600 dark:text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8c-1.7 0-3 .9-3 2s1.3 2 3 2 3 .9 3 2-1.3 2-3 2m0-8V7m0 1v8m0 0v1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<div class="mt-4 flex items-center gap-1.5">
<span class="flex items-center gap-1 text-sm font-semibold text-green-600 dark:text-green-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
+12.5%
</span>
<span class="text-xs text-gray-400 dark:text-gray-500">較上個月</span>
</div>
</div>
<!-- 卡片:訂單數 -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">本月訂單數</p>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">328</p>
</div>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
bg-purple-50 dark:bg-purple-900/20">
<svg class="h-5 w-5 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/>
</svg>
</div>
</div>
<div class="mt-4 flex items-center gap-1.5">
<span class="flex items-center gap-1 text-sm font-semibold text-green-600 dark:text-green-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
+5.2%
</span>
<span class="text-xs text-gray-400 dark:text-gray-500">較上個月</span>
</div>
</div>
<!-- 卡片:新用戶(負向趨勢,紅) -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">新增用戶</p>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">42</p>
</div>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
bg-amber-50 dark:bg-amber-900/20">
<svg class="h-5 w-5 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.4-1.9M17 20H7m10 0v-2c0-.7-.1-1.3-.4-1.9M7 20H2v-2a3 3 0 015.4-1.9M7 20v-2c0-.7.1-1.3.4-1.9m0 0a5 5 0 019.3 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
</div>
<div class="mt-4 flex items-center gap-1.5">
<span class="flex items-center gap-1 text-sm font-semibold text-red-500 dark:text-red-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"/>
</svg>
-3.1%
</span>
<span class="text-xs text-gray-400 dark:text-gray-500">較上個月</span>
</div>
</div>
<!-- 卡片:轉換率 -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">轉換率</p>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">3.24%</p>
</div>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg
bg-green-50 dark:bg-green-900/20">
<svg class="h-5 w-5 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
</div>
</div>
<div class="mt-4 flex items-center gap-1.5">
<span class="flex items-center gap-1 text-sm font-semibold text-green-600 dark:text-green-400">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
+0.3%
</span>
<span class="text-xs text-gray-400 dark:text-gray-500">較上個月</span>
</div>
</div>
</div>
這排卡片的關鍵在外層那行 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4:它讓四張卡在手機疊成一欄、平板兩欄一組、大螢幕才攤成一整排。用 grid 而不是 flex 的理由是——grid 的每一欄天生等寬、換行後自動對齊,不必額外算 basis 或 w-1/4,是等寬卡片牆的最佳工具。每張卡內部則是一個小型 flex:標題數值在左、圖示容器在右(justify-between),下方趨勢列用不同顏色(綠 text-green-600 / 紅 text-red-500)與方向相反的箭頭 SVG 表達漲跌。留意圖示底色也對應語意(收入用品牌 cyan、用戶用 amber),讓一整排卡片有顏色層次而不單調。
圖表版位:純 CSS 佔位,等函式庫接上
後台幾乎都有圖表,但版面通常先做、圖表資料後串。這段示範怎麼用 Tailwind 做一個圖表佔位卡:外層是標準卡片,裡面用固定高度容器當畫布,再用純 CSS 疊出示意的長條圖,等 Chart.js 或 ECharts 接上,直接把佔位內容換成 <canvas> 即可。左右兩張卡用 grid 排成不等寬的 2:1 佈局。
<!-- 圖表區:左寬右窄(2:1),手機疊成一欄 -->
<div class="mb-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
<!-- 主圖表(佔 2 欄) -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900 lg:col-span-2">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-gray-900 dark:text-white">營收趨勢</h3>
<span class="text-xs text-gray-400 dark:text-gray-500">近 7 天</span>
</div>
<!-- 畫布區:固定高度,用假長條圖佔位;之後換成 <canvas> -->
<div class="flex h-56 items-end gap-2">
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 45%"></div>
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 70%"></div>
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 55%"></div>
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 85%"></div>
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 60%"></div>
<div class="flex-1 rounded-t bg-cyan-500" style="height: 100%"></div>
<div class="flex-1 rounded-t bg-cyan-500/80" style="height: 75%"></div>
</div>
</div>
<!-- 副圖表(佔 1 欄):示意甜甜圈/占比 -->
<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm
dark:border-gray-800 dark:bg-gray-900">
<h3 class="mb-4 text-base font-semibold text-gray-900 dark:text-white">流量來源</h3>
<div class="flex h-56 flex-col justify-center gap-4">
<!-- 用進度條佔位占比 -->
<div>
<div class="mb-1 flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">自然搜尋</span>
<span class="font-medium text-gray-900 dark:text-white">58%</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
<div class="h-full rounded-full bg-cyan-500" style="width: 58%"></div>
</div>
</div>
<div>
<div class="mb-1 flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">社群媒體</span>
<span class="font-medium text-gray-900 dark:text-white">27%</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
<div class="h-full rounded-full bg-purple-500" style="width: 27%"></div>
</div>
</div>
<div>
<div class="mb-1 flex justify-between text-sm">
<span class="text-gray-600 dark:text-gray-400">直接流量</span>
<span class="font-medium text-gray-900 dark:text-white">15%</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
<div class="h-full rounded-full bg-amber-500" style="width: 15%"></div>
</div>
</div>
</div>
</div>
</div>
圖表版位的兩個重點都是為了「之後好接函式庫」而設計。第一是外層用 lg:grid-cols-3 搭 lg:col-span-2:主圖表跨兩欄、副圖表佔一欄,做出後台常見的 2:1 版面,手機則各自疊成整欄。第二是畫布區的固定高度 h-56:圖表函式庫需要一個有明確高度的容器才能正確渲染,先用 h-56 定好,裡面暫時用 flex items-end 的一排 div 當假長條圖(高度用 inline style 或 h-* 表示數值)。等真的接上 Chart.js,只要把 h-56 容器內的假 div 換成 <canvas>,外層卡片、標題、間距、暗色模式全都不用動——版面先定、資料後填,這是後台開發最順的節奏。
資料表格:標題列、狀態徽章與水平捲動
最後是後台的重頭戲——資料表格。它要能塞下多欄資料、有可讀的狀態徽章、還要在手機不爆版。整塊包在一張卡裡:上方標題列(標題 + 搜尋 + 匯出)、中間表格本體(用 overflow-x-auto 包住做水平捲動)、下方分頁。
<!-- 資料表格卡 -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm
dark:border-gray-800 dark:bg-gray-900">
<!-- 標題列 -->
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 px-5 py-4
dark:border-gray-800">
<div>
<h3 class="text-base font-semibold text-gray-900 dark:text-white">最近訂單</h3>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">共 328 筆,顯示最新 10 筆</p>
</div>
<button class="flex items-center gap-2 rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium
text-gray-700 hover:bg-gray-50
dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
匯出
</button>
</div>
<!-- 表格本體:overflow-x-auto 讓寬表格在區塊內水平捲動 -->
<div class="overflow-x-auto">
<table class="w-full min-w-[640px] table-fixed">
<colgroup>
<col class="w-32"><col class="w-56"><col class="w-32">
<col class="w-32"><col class="w-32">
</colgroup>
<thead>
<tr class="border-b border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-800/50">
<th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">訂單號</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">客戶</th>
<th class="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">金額</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">狀態</th>
<th class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">日期</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<!-- 完成 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-5 py-4"><span class="font-mono text-sm font-medium text-cyan-600 dark:text-cyan-400">#ORD-1001</span></td>
<td class="px-5 py-4">
<div class="flex min-w-0 items-center gap-3">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-cyan-600 text-xs font-bold text-white">陳</div>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">陳小明</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">chen@example.com</p>
</div>
</div>
</td>
<td class="px-5 py-4 text-right text-sm font-semibold text-gray-900 dark:text-white">$1,290</td>
<td class="px-5 py-4">
<span class="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-2.5 py-1 text-xs font-medium
text-green-700 dark:bg-green-900/30 dark:text-green-400">
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>已完成
</span>
</td>
<td class="px-5 py-4 text-sm text-gray-500 dark:text-gray-400">2026/09/01</td>
</tr>
<!-- 處理中 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-5 py-4"><span class="font-mono text-sm font-medium text-cyan-600 dark:text-cyan-400">#ORD-1002</span></td>
<td class="px-5 py-4">
<div class="flex min-w-0 items-center gap-3">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-purple-600 text-xs font-bold text-white">李</div>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">李大華</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">li@example.com</p>
</div>
</div>
</td>
<td class="px-5 py-4 text-right text-sm font-semibold text-gray-900 dark:text-white">$890</td>
<td class="px-5 py-4">
<span class="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium
text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
<span class="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500"></span>處理中
</span>
</td>
<td class="px-5 py-4 text-sm text-gray-500 dark:text-gray-400">2026/09/02</td>
</tr>
<!-- 失敗 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<td class="px-5 py-4"><span class="font-mono text-sm font-medium text-cyan-600 dark:text-cyan-400">#ORD-1003</span></td>
<td class="px-5 py-4">
<div class="flex min-w-0 items-center gap-3">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-green-600 text-xs font-bold text-white">王</div>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">王美玉</p>
<p class="truncate text-xs text-gray-500 dark:text-gray-400">wang@example.com</p>
</div>
</div>
</td>
<td class="px-5 py-4 text-right text-sm font-semibold text-gray-900 dark:text-white">$2,100</td>
<td class="px-5 py-4">
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-100 px-2.5 py-1 text-xs font-medium
text-red-700 dark:bg-red-900/30 dark:text-red-400">
<span class="h-1.5 w-1.5 rounded-full bg-red-500"></span>失敗
</span>
</td>
<td class="px-5 py-4 text-sm text-gray-500 dark:text-gray-400">2026/09/02</td>
</tr>
</tbody>
</table>
</div>
<!-- 分頁 -->
<div class="flex items-center justify-between border-t border-gray-200 px-5 py-4 dark:border-gray-800">
<p class="text-sm text-gray-500 dark:text-gray-400">
顯示 <span class="font-medium text-gray-900 dark:text-white">1–10</span> /
<span class="font-medium text-gray-900 dark:text-white">328</span> 筆
</p>
<div class="flex items-center gap-2">
<button disabled
class="rounded-md border border-gray-200 px-3 py-1.5 text-sm text-gray-500
disabled:cursor-not-allowed disabled:opacity-40
dark:border-gray-700 dark:text-gray-400">上一頁</button>
<span class="rounded-md bg-cyan-500 px-3 py-1.5 text-sm font-medium text-white">1</span>
<button class="rounded-md border border-gray-200 px-3 py-1.5 text-sm text-gray-500
hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors">下一頁</button>
</div>
</div>
</div>
表格這段有三個必記的重點。第一是 overflow-x-auto 包 table 加 min-w-[640px]:給表格一個最小寬度,在手機放不下時,表格會在自己的容器內水平捲動,而不會撐破整個主內容區(還記得外殼那個 min-w-0 嗎?兩者搭配才能讓捲動真正生效)。第二是 table-fixed 搭 colgroup:固定各欄寬度,捲動或內容長短不一時欄位不會亂跳,對齊穩定。第三是狀態徽章:用 inline-flex 的膠囊(圓角 + 淺底色 + 小圓點)把「已完成 / 處理中 / 失敗」三種狀態視覺化,綠、琥珀、紅一眼可辨,處理中還加 animate-pulse 讓小圓點閃爍暗示「進行中」。至此,側邊欄、頂部列、KPI 卡片、圖表版位、資料表格五大塊全部到位,一個完整的後台儀表板骨架就成形了。
順帶補上讓側欄在手機收合的那一小段 JavaScript——版面本身純 CSS 就完成了,只有這個開關需要它:
// hamburger 切換側欄滑入/滑出 + 遮罩
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
document.getElementById('toggle').addEventListener('click', () => {
sidebar.classList.remove('-translate-x-full'); // 滑入
overlay.classList.remove('hidden'); // 顯示遮罩
});
overlay.addEventListener('click', () => {
sidebar.classList.add('-translate-x-full'); // 滑出
overlay.classList.add('hidden'); // 隱藏遮罩
});
常見錯誤與最佳實踐
後台的坑跟前兩篇很不一樣,幾乎都圍繞「固定外殼」與「資訊密度」打轉。
坑一:主內容區忘了 min-w-0,寬表格把側欄擠爆
這是後台版面第一名的坑,而且極難 debug。你用 flex 讓側欄與主內容並排,一切正常——直到主內容裡放進一張寬表格,整個版面就崩了:側欄被壓扁、水平捲動也失效。原因是 flex item 預設 min-width: auto,它會拒絕縮小到內容寬度以下,於是寬表格硬把父容器撐開。正解是給主內容區加 min-w-0,允許它正常收縮,裡面的 overflow-x-auto 才會真正生效。記住這條:凡是 flex 容器裡放了可能很寬的內容,子項就要 min-w-0。
坑二:側邊欄響應式只做一半,手機被側欄佔滿
有些人桌機側欄做得很漂亮,一到手機整個畫面被 w-64 側欄佔掉大半,內容擠成一條。正解是同一份 markup 用斷點切換兩種模式:手機用 fixed -translate-x-full 把側欄藏到畫面外、預設不佔空間,配 hamburger 滑入與遮罩;桌機才用 lg:relative lg:translate-x-0 讓它常駐佔一欄。別讓一個固定寬側欄在手機硬吃掉半個螢幕。
坑三:資料表格硬塞進手機,欄位擠到不可讀
後台表格欄位動輒七八個,想在手機全部顯示是不切實際的。硬塞的結果是文字換行、對齊全亂、根本讀不了。正解分兩層:基本做法是 overflow-x-auto 包住表格加 min-w-[640px],讓它水平捲動、保住欄位對齊;更進階可以用 hidden sm:table-cell 在小螢幕隱藏次要欄位、只留關鍵資訊。別為了「全部塞進去」而犧牲可讀性。
坑四:資訊密度失控,卡片與間距一樣鬆散
後台跟行銷頁的美學相反:行銷頁要大留白求氣勢,後台要適當緊湊求效率。有些人把行銷頁那套 p-8、gap-12、text-lg 搬到後台,結果一屏看不到幾筆資料,使用者得一直捲。正解是提高資訊密度:卡片用 p-5、格線 gap-4、表格列 py-4、輔助文字 text-sm/text-xs。後台的專業感來自「一屏看得到夠多資訊、又不擁擠」的平衡,而不是大而空的留白。
最佳實踐小結
- flex 子項記得 min-w-0:主內容區放寬內容時必加,否則側欄被撐爆。
- 側欄雙模式:桌機
lg:relative常駐、手機fixed -translate-x-full抽屜式,配遮罩與動畫。 - 表格水平捲動:
overflow-x-auto包table加min-w-*,別硬塞進手機。 - 等寬卡片用 grid:KPI 卡用
grid-cols-*換欄,天生等寬、對齊免算。 - 圖表先佔位:固定高度容器先定版面,之後換
<canvas>不動排版。 - 控制資訊密度:後台用較小的 padding、間距與字級,一屏塞得下更多資訊。
- 暗色成對出現:每個
bg-、text-、border-都補一組dark:,徽章用/30透明底。
小結
這是 TailwindCSS 完整教學 系列的第四十五篇,也是實戰案例的第三課、最後一堂。上一篇《案例:部落格》我們把重點放在 prose 與內容排版,主體是單純好讀的長文;這一篇則轉向結構最硬的後台儀表板,主角換成 flex 與 grid 的綜合排版,以及「固定外殼 + 可捲動內容」這種應用程式級的版面骨架。回顧幾個重點:
- 版面拆解:整頁不捲動,只有主內容區捲動;外殼用
flex h-screen,側欄固定、主內容flex-1 min-w-0。 - 逐段實作:側邊欄(垂直 flex 三段式 + group 導覽)、頂部列(hamburger + flex-1 搜尋 + ml-auto 工具)、KPI 卡片(responsive grid)、圖表版位(固定高度佔位卡)、資料表格(overflow-x-auto + 狀態徽章 + 分頁)。
- 後台心法:
min-w-0防撐爆、側欄雙模式收合、表格水平捲動、grid 做等寬卡、控制資訊密度。 - 後台的坑:忘了
min-w-0、側欄響應式做一半、表格硬塞手機、資訊密度失控,以及對應的正確做法。
到這裡,本系列三堂實戰案例——講究轉換的 Landing Page、講究閱讀的部落格、講究資訊密度的後台儀表板——全部走完了。這三種頁面的版面哲學截然不同,卻共用同一套 Tailwind 工具:flex、grid、響應式斷點、dark:、狀態變體。當你能用同一組 utility 打造出風格與需求都完全相反的三種作品,就代表你真正把 TailwindCSS 內化成了自己的工具,而不只是背 class。從基礎語法到綜合實戰,一路走到這裡,你的 Tailwind 功力已經完整。接下來,就把它們用到你自己的專案上吧。實戰之路,繼續往前。