HeatMapXHeatMapX
價格登入

用 Claude Code 一天重建 SaaS 儀表板 — Tailwind v4 + CSS Grid 表格對齊

HeatMapX Engineering Team18 min read
  • engineering
  • tailwind
  • css-grid
  • dark-mode
  • claude-code

TL;DR

  • 問題:15 位新用戶中有 11 位(73%)完成註冊後卻沒有新增任何 URL 就離開了
  • 假設:我們類別中的每款熱圖工具預設都使用淺色 UI——我們的深色儀表板可能造成困惑與早期流失
  • 我們做了什麼:一天內完成完整儀表板重設計——淺色 / 深色 / 系統三向切換 + GitHub 風格表格列表
  • 學到的事:Tailwind v4 的 @custom-variant、CSS Grid auto 欄陷阱、對抗長文字的三層防禦,以及為何 10–16 次設計迭代是常態而非例外
  • 結果:流失率影響待評估;兩週數據後將發布後續報告

背景:我們是否因為儀表板太暗而流失用戶?

HeatMapX(heatmapx.com)是一款可透過 Claude Code CLI 呼叫的熱圖與 CRO 工具。在展開第一波推廣後,我們發現了一個規律:15 位新用戶中有 11 位(73%)完成了註冊,卻沒有新增任何一個 URL 就離開了。

其中一個假設:我們類別中所有主要熱圖工具都預設使用淺色 UI。HeatMapX 卻是以深色管理介面示人的異類。第一次進入的用戶在一片白色主題的應用程式中看到深色儀表板,可能就因為困惑而跳出了。

於是我們與 Claude Code 合作,在一個工作階段內完成了完整重設計。本文涵蓋假設驗證的前半部分——診斷流失問題並執行重設計。後續篇章將在部署後兩週、取得實際數據後發布。

最終設計配置

元素 淺色 深色
頁面背景 bg-neutral-100(#f5f5f5) dark:bg-neutral-950(#0a0a0a)
卡片表面 bg-white + border-slate-200 dark:bg-slate-900 + dark:border-slate-800
主要文字 text-slate-900 dark:text-slate-100
主色(CTA) bg-orange-600(#ea580c) 兩種模式共用
圓角半徑 rounded-md(6px)· 無陰影 · GitHub 風格
主題切換 ☀ / 💻 / 🌙 三向切換置於 header

Tailwind v4 深色模式——從零配置到手動切換

從 @media prefers-color-scheme 開始

Tailwind v4 的預設行為是將 dark: 變體直接對應到 @media (prefers-color-scheme: dark)。開箱即用、零配置,它會直接跟隨作業系統的深色模式設定。

{/* layout.tsx */}
<div className="bg-neutral-100 text-slate-900 dark:bg-neutral-950 dark:text-slate-100">
  ...
</div>

這樣就能處理「跟隨系統」的情境(例如夜間自動切換深色),完全不需要額外設定。但用戶有時會想覆蓋系統設定——「系統」模式可能判斷有誤,或純粹是個人偏好——因此我們將其擴充為三向切換。

切換為以 class 為基礎的深色變體

在 Tailwind v4 中,你可以使用 @custom-variant 重新定義 dark: 變體的行為。我們切換到了只有在 <html> 上存在 .dark 時,dark: 才會觸發的模式:

/* globals.css */
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

:where() 包裝器將選擇器優先權保持為 0。若不加這個,深色變體樣式可能與樣式表中其他地方的既有優先權規則衝突。

防止 SSR 閃爍的同步腳本

如果在 React 水合(hydration)完成後才在 useEffect 中套用主題,會產生未樣式化內容閃爍(FOUC)——首次渲染時短暫出現白色閃光。解決方式是在 <head> 中注入一段行內同步腳本,使其在 body 渲染前執行:

{/* app/layout.tsx */}
<head>
  <script dangerouslySetInnerHTML={{ __html: `
    (function(){try{
      var t = localStorage.getItem('theme') || 'system';
      var d = t === 'dark' || (t === 'system' &&
        window.matchMedia('(prefers-color-scheme: dark)').matches);
      if (d) document.documentElement.classList.add('dark');
    }catch(e){}})();
  `}} />
</head>

這段程式碼在 React 掛載前同步執行,因此當 body 開始渲染時,.dark 已經在 <html> 元素上了。在 <html> 上加入 suppressHydrationWarning 以消除水合不匹配的警告。

💡 學習 1:Tailwind v4 的 dark: 零配置即可運作。如果只需要跟隨 OS,完全不需要動任何東西。只有在明確需要用戶手動切換時,才加入 @custom-variant + 同步腳本——這是一個乾淨的兩階段做法。

CSS Grid 表格對齊——以及 auto 欄如何咬你一口

單欄列表不夠用

在收到「新增網站後的網站卡片感覺不對」的反饋後,我們先嘗試了顯而易見的做法:把兩欄卡片格換成單欄列表。每個 SiteCard 內部使用 flexbox 來排列 name · url · status · events,右側有一個箭頭符號。

問題在多個項目出現時就暴露了:

Short · example.com · ● Active · 5,577
Very Long Site Name That... · normal.com · ● Active · 5,577
ACME · acme.io · ● Active · 5,577

· Active· 5,577› 箭頭每一行都落在不同的 x 座標。眼睛無法錨定「狀態欄在這裡」。這不像一個列表——它讀起來像是一堆垂直堆疊的字串。

這根本不是列表,是文字,垂直堆疊。

轉換為 CSS Grid 表格佈局

我們用固定欄位定義的 CSS Grid 取代了 flexbox 內部結構。由於每張卡片共用相同的 grid 模板,欄位左邊緣在整個頁面上完美對齊:

// SiteCard.tsx
<Link className="grid grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1.5fr)_minmax(0,3fr)_110px_72px_20px] items-center gap-x-4 ...">
  <h3 className="truncate">{name}</h3>
  <span className="truncate">{displayUrl}</span>
  <span className="truncate font-mono">{apiKey}</span>
  <span className="truncate font-mono">{trackerSnippet}</span>
  <span>● Active</span>
  <span>{count}</span>
  <span>›</span>
</Link>

標頭列使用完全相同的 grid 模板

// page.tsx 標頭列
<div className="grid grid-cols-[minmax(0,2fr)_minmax(0,2fr)_minmax(0,1.5fr)_minmax(0,3fr)_110px_72px_20px] gap-x-4 ...">
  <span>SITE</span>
  <span>URL</span>
  <span>API KEY</span>
  <span>TRACKER TAG</span>
  <span>STATUS</span>
  <span>TODAY</span>
  <span></span>
</div>

陷阱:不同 grid 容器中的 auto 欄各自獨立計算尺寸

我們的第一版使用了 grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto]。當時的想法是:「auto 會依據內容調整,而且因為標頭和 SiteCard 使用相同的模板字串,它們應該會對齊。」

它們沒有對齊。原因如下:

  • 標頭的 auto 欄:依據 "STATUS" 文字寬度計算
  • SiteCard 的 auto 欄:依據 "● Active" 文字寬度計算
  • 兩者都是獨立的 grid 容器——軌道尺寸各自獨立計算

兩種解決方案:

  1. 固定寬度:用明確的像素值如 110px72px 取代 auto(我們最終採用的方案)
  2. CSS Subgrid:將標頭和所有 SiteCard 放在同一個父 grid 內,然後透過 subgrid 讓每一列繼承父容器的軌道(更複雜)

💡 學習 2:當你需要跨不同 grid 容器進行表格對齊時,不要使用 auto 欄。每個容器依據自身內容獨立計算軌道寬度。請使用固定像素寬度,或透過 subgrid 共享軌道。

對抗長文字——三層防禦

初始的資料庫結構是這樣的:

CREATE TABLE sites (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,  -- 無長度限制
  url text NOT NULL,   -- 無長度限制
  ...
);

Postgres 的 text 實際上沒有長度限制。一個 10,000 個字元的網站名稱是完全合法的 SQL——但會立刻摧毀表格佈局。轉換為固定欄位 grid 後,文字長度限制變得不可妥協。

我們最終採用的三層防禦

  1. 前端 HTML5 限制:輸入框上的 maxLength={60} / maxLength={512}
  2. 伺服器端驗證:Server Action 檢查 name.length > 60 並回傳錯誤
  3. 顯示截斷:每個 grid 儲存格上的 truncate class,搭配 minmax(0, ...) 以啟用溢出處理

第三層是人們最常忽略的。truncate 展開為 overflow: hidden + text-overflow: ellipsis + white-space: nowrap,但在 flex 或 grid 的情境下,儲存格還需要 min-width: 0——否則儲存格會擴張以容納內容,溢出永遠不會觸發。minmax(0, ...) 正好提供了這一點。

16 次設計迭代

光是 SiteCard 在這個工作階段就經歷了 16 次設計迭代。部分關鍵轉折:

# 變更 原因
1 將 CopyBlock 轉換為淺灰色膠囊樣式 黑色背景 + 綠色文字在 GitHub 淺色模式下看起來很奇怪
3 A/B/C/D 四選項比較 需要更強的點擊引導
4 切換為單欄列表 被否決為「太花俏」——回歸基本
7 移除「View Heatmap →」CTA 「看起來不夠乾淨」
9 轉換為 CSS Grid 表格佈局 以分隔符號為基礎的字串佈局「感覺不像列表」
10 auto → 固定像素寬度 標頭和卡片的欄位沒有對齊
11 恢復 API KEY 欄 「等等,API key 在哪裡?」
12 新增 TRACKER TAG 欄 「把兩個並排放」
14 新增 maxLength + 伺服器驗證 針對長文字健壯性的正確修復
16 在標題旁新增「+」按鈕(緊湊模式) 不需捲動就能快速新增

不要期望一次就能成功。 設計迭代意味著「在開發伺服器中檢查 → 立即反饋 → 下一次嘗試」,重複 10+ 次。請預先規劃這個循環。像 Claude Code 這樣的互動式 AI 開發環境正是讓這個模式真正發光的地方。

與 AI 輔助開發搭配時真正有效的模式

這些不是工具特有的觀察——而是與互動式 AI 配對時被證實有效的通用模式:

  • 新 API 語法的查找成本為零:Tailwind v4 的 @custom-variant 相對較新。Claude Code 立即以可運行的形式提供了它,無需翻閱文件。
  • 實作 → 驗證 → 修復的循環在秒內完成:重現 CSS Grid auto 未對齊問題、隔離原因、交付修復,總共只花了約 5 分鐘。
  • 機械性的批量工作蒸發了:在單次迭代中替換 15 個語系檔案中的佔位文字(in-golexample.com)。
  • 與 HMR 的親和性:編輯 → 儲存 → 瀏覽器即時刷新 → 反饋 → 再次編輯。這個循環夠快,讓你保持在心流狀態。
  • 一個工作階段端到端完成:從完成重設計直接到推送到正式環境並觀看 Vercel 部署——沒有任何情境切換。

下一節涵蓋效果較差的部分。

哪些地方困難 / 還有什麼待完成

  • 將模糊的反饋轉化為結構性決策讓迭代次數倍增。「這感覺不對」必須先變成「表格 vs. 列表 vs. 卡片」才能進行任何有用的工作。事先達成結構共識可以節省 3–4 次迭代。
  • 在第 10 次迭代前後,需求不斷擴展——「全部內聯」、「全部截斷」、「所有欄位」——而某些限制確實存在張力(資訊密度 vs. 可瀏覽性)。沒有捷徑能通往最終形式。
  • 行動版尚未處理。 7 欄的 grid 在小螢幕上會產生水平捲動。解決方式是使用 sm: 媒體查詢來堆疊欄位,或使用獨立的行動版元件。這是下一個要處理的項目。

結論

在一個工作階段內,我們將 HeatMapX 儀表板從深色卡片格重建為具有 GitHub 風格表格列表的淺色/深色/系統切換佈局。關鍵技術收穫:

  1. Tailwind v4 深色模式:從 @media 基礎的深色模式開始——零配置。只有在用戶需要手動覆蓋時,才加入 @custom-variant + 同步腳本。
  2. CSS Grid 表格對齊auto 欄不能跨不同 grid 容器對齊。使用固定像素寬度或 CSS Subgrid——這是唯一可靠的選擇。
  3. 長文字健壯性:三層,缺一不可——前端 maxLength、伺服器端長度檢查,以及帶 minmax(0, ...) 的顯示 truncate
  4. 設計迭代預算:假設每個元件需要 10–16 次迭代,並從一開始就建立快速的反饋循環。

這是否真的能改善流失率仍是一個開放的問題。我們將在兩週實際數據後發布後續報告。

從 Claude Code 執行的熱圖,免費開始。

貼上一行追蹤標籤,從 CLI 取得分析與改善建議。