案例:儀表板 — 打造後台 Dashboard 版面 | TailwindCSS 完整教學

2026/09/14
案例:儀表板 — 打造後台 Dashboard 版面 | TailwindCSS 完整教學

前兩篇我們做了講究「轉換」的行銷 Landing Page 與講究「閱讀」的內容型部落格,這一篇要挑戰版面最複雜、也最考驗 flexgrid 綜合功力的一種頁面——管理後台 Dashboard。我們會用 TailwindCSS v4 逐段打造一個可收合的固定側邊欄、sticky 頂部列、一整排 KPI 統計卡片、可捲動的資料表格,以及圖表版位,並認真處理行動版側欄滑入、表格 RWD 與暗色模式。做完你會得到一個能直接放後台功能上去的完整儀表板骨架。

前言

如果說 Landing Page 練的是「氣勢」、部落格練的是「閱讀」,那後台儀表板練的就是「資訊密度」。這是三種版面裡結構最硬的一種:畫面被切成側邊導覽、頂部工具列、內容區三大塊,內容區裡又塞著統計卡、圖表、表格,每一塊都有自己的捲動、對齊與響應式規則。它是 flexgrid 的綜合考題,也是每個做過管理系統的前端都逃不掉的日常。

這是本系列實戰案例的第三課,也是最後一堂。上一篇《案例:部落格》我們把重點放在 prose 與內容排版,主體是單純的三欄長文;這一篇的挑戰完全不同——後台版面是「固定外殼 + 可捲動內容」的組合。側邊欄要釘死、頂部列要黏住、只有中間主內容區跟著捲動,這種「框架不動、內容流動」的結構,正是所有 IDE、後台、Email 客戶端的共通骨架。學會它,你就掌握了應用程式級版面的基本功。

為什麼儀表板值得單獨做一課?因為它把前面學過的東西一次全用上,還逼你面對兩個新難題。第一是固定外殼的佈局:如何用 flex h-screen 讓側邊欄與主內容並排、各自處理自己的捲動,而不是整頁一起捲。第二是後台特有的響應式:桌機側邊欄常駐、手機要收成抽屜式滑入,以及資料表格在小螢幕怎麼不爆版。這兩題在行銷頁和部落格都不太會遇到,卻是後台的核心。

成品是一個單頁儀表板。左側是固定的側邊欄,有 LOGO、分組導覽選單、底部系統狀態與使用者資訊,手機上可收合成抽屜。頂部是一條 sticky 的工具列,放 hamburger、搜尋、通知與使用者選單。主內容區由上而下是:一排響應式的 KPI 統計卡片、一塊圖表版位,以及一張帶標題、搜尋、狀態徽章與分頁的資料表格。整頁完全響應式並支援暗色模式。

本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。

本篇你將學到:

  • 固定外殼佈局:用 flex h-screen overflow-hiddenmin-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 黏住,下方 mainflex-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 的每一欄天生等寬、換行後自動對齊,不必額外算 basisw-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-3lg:col-span-2:主圖表跨兩欄、副圖表佔一欄,做出後台常見的 2:1 版面,手機則各自疊成整欄。第二是畫布區的固定高度 h-56:圖表函式庫需要一個有明確高度的容器才能正確渲染,先用 h-56 定好,裡面暫時用 flex items-end 的一排 div 當假長條圖(高度用 inline styleh-* 表示數值)。等真的接上 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-autotablemin-w-[640px]:給表格一個最小寬度,在手機放不下時,表格會在自己的容器內水平捲動,而不會撐破整個主內容區(還記得外殼那個 min-w-0 嗎?兩者搭配才能讓捲動真正生效)。第二是 table-fixedcolgroup:固定各欄寬度,捲動或內容長短不一時欄位不會亂跳,對齊穩定。第三是狀態徽章:用 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-8gap-12text-lg 搬到後台,結果一屏看不到幾筆資料,使用者得一直捲。正解是提高資訊密度:卡片用 p-5、格線 gap-4、表格列 py-4、輔助文字 text-sm/text-xs。後台的專業感來自「一屏看得到夠多資訊、又不擁擠」的平衡,而不是大而空的留白。

最佳實踐小結

  • flex 子項記得 min-w-0:主內容區放寬內容時必加,否則側欄被撐爆。
  • 側欄雙模式:桌機 lg:relative 常駐、手機 fixed -translate-x-full 抽屜式,配遮罩與動畫。
  • 表格水平捲動:overflow-x-autotablemin-w-*,別硬塞進手機。
  • 等寬卡片用 grid:KPI 卡用 grid-cols-* 換欄,天生等寬、對齊免算。
  • 圖表先佔位:固定高度容器先定版面,之後換 <canvas> 不動排版。
  • 控制資訊密度:後台用較小的 padding、間距與字級,一屏塞得下更多資訊。
  • 暗色成對出現:每個 bg-text-border- 都補一組 dark:,徽章用 /30 透明底。

小結

這是 TailwindCSS 完整教學 系列的第四十五篇,也是實戰案例的第三課、最後一堂。上一篇《案例:部落格》我們把重點放在 prose 與內容排版,主體是單純好讀的長文;這一篇則轉向結構最硬的後台儀表板,主角換成 flexgrid 的綜合排版,以及「固定外殼 + 可捲動內容」這種應用程式級的版面骨架。回顧幾個重點:

  • 版面拆解:整頁不捲動,只有主內容區捲動;外殼用 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 工具:flexgrid、響應式斷點、dark:、狀態變體。當你能用同一組 utility 打造出風格與需求都完全相反的三種作品,就代表你真正把 TailwindCSS 內化成了自己的工具,而不只是背 class。從基礎語法到綜合實戰,一路走到這裡,你的 Tailwind 功力已經完整。接下來,就把它們用到你自己的專案上吧。實戰之路,繼續往前。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →