Claude Code で SaaS ダッシュボードを 1 日で再設計した記録 — Tailwind v4 + CSS Grid タブラーアラインメント

ヒートマップエックス エンジニアチーム19分で読了
  • engineering
  • tailwind
  • css-grid
  • dark-mode
  • claude-code

TL;DR

  • 課題: 新規ユーザー 15人中 11人 (73%) が URL登録せず離脱
  • 仮説: 業界のヒートマップツールが全てライトUIなのに HeatMapX だけ暗いダッシュボードで戸惑い離脱
  • 対応: 1日で全面再設計 — ライト/ダーク/システム 3択 + GitHub風タブラーリスト
  • 学び: Tailwind v4 @custom-variant、CSS Grid auto 列の落とし穴、長文耐性の3層防御、デザイン反復は 10–16 ターン前提
  • 効果検証: 2週間後のフォローアップ記事で離脱率の変化を測定予定(本記事時点では未測定)

背景:暗いダッシュボードが離脱されている疑い

HeatMapX (heatmapx.com) は Claude Code から CLI で呼べるヒートマップ&CRO ツールです。プロモーション展開を始めたところ、新規ユーザー 15 人中 11 人 (73%) が「サインアップしたが URL を登録しないまま離脱」する現象が観測されました。

仮説のひとつが「業界のヒートマップツールは全て ライト UI デフォルト で、HeatMapX だけ暗い管理画面のため、初見ユーザーが戸惑って離脱しているのでは」というものでした。

そこで、Claude Code を相棒に 1 日のセッション内でダッシュボードを全面再設計しました。本記事は仮説検証の前半(離脱要因の調査と再設計)の記録です。改善後の数値が出たら続編でフォローアップします。

採用した最終構成

要素 Light Dark
ページ背景 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 風
テーマ切替 ヘッダー右側に ☀ / 💻 / 🌙 の3択トグル

Tailwind v4 のダークモード — Zero-Config から手動切替へ

最初は @media prefers-color-scheme で済ませた

Tailwind v4 のデフォルト挙動は dark: variant が @media (prefers-color-scheme: dark) と等価。これは追加設定なしで OS のダークモード設定に追随します。

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

これで OS の外観設定(夜間自動ダーク等)に追随する実装は完了。ただし、ユーザーが明示的にライト/ダークを選びたいケース ("system" が誤動作する、好みの問題) があるため、3 択トグル方式に拡張しました。

class-based dark variant への切替

Tailwind v4 では @custom-variant で dark の挙動を再定義できます。<html>.dark クラスが付いた時のみ dark: を発火させる方式に変更:

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

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

:where() で specificity を 0 に抑えているのがポイント。これがないと既存のスタイル指定と競合する場合があります。

SSR-flash 防止の同期スクリプト

React の hydration 後に useEffect でテーマを適用すると、初回レンダで一瞬白くなる "FOUC" が発生します。これを防ぐため、<head> に同期スクリプトを inline で挿入:

{/* 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 mount 前に同期実行されるので、body レンダリング時点で既に .dark クラスが付いた状態。suppressHydrationWarning<html> に付けて hydration mismatch を抑制。

💡 学び1: Tailwind v4 の dark: は zero-config で動く。シンプルに OS 連動のみで良ければ追加設定不要。手動切替を加える時のみ @custom-variant + 同期スクリプトを追加する 2 段階アプローチが効率的。

CSS Grid タブラーアラインメントの落とし穴

初期:1-column リストで満足できなかった

「サイト追加後のカードが微妙」という指摘を受け、最初は単純に 2-col grid → 1-col リストに変えました。各 SiteCard は flex で内部に name · url · status · events を並べ、右端に chevron。

しかし複数件並べて気付いた:

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

· 計測中」「· 5,577」「 chevron」が 毎行違う x 座標 に出現。目が「ステータス列はここ」と固定できず、リストとして スキャンできない

これは「リストではなく、文字列が縦に積まれているだけ」だった。

CSS Grid タブラーレイアウトへの転換

各 SiteCard 内部を CSS Grid で固定列定義 に変更。全カードが同一の grid template を持つので、列の左端が縦に完全一致する:

// 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>● 計測中</span>
  <span>{count}</span>
  <span>›</span>
</Link>

ヘッダー行も 完全同一の grid template で書く:

// 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>

落とし穴: auto カラムは grid 別なら別サイズ

最初は grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto] と書きました。「auto なら中身に合わせて伸縮するし、ヘッダーと SiteCard は同じ template なら揃うだろう」と。

結果:揃わない。なぜなら:

  • ヘッダーの auto 列: "STATUS" / "ステータス" のテキスト幅で決定
  • SiteCard の auto 列: "● 計測中" / "● Active" のテキスト幅で決定
  • 両者は 別の grid container なので、track size は独立計算される

解決策は 2 つ:

  1. 固定幅指定: auto110px, 72px 等のピクセル値に変更(採用)
  2. CSS Subgrid: ヘッダーと全 SiteCard を単一の親 grid 内に入れ、各 row が parent の track を subgrid で継承(複雑)

💡 学び2: タブラーアラインメントでは auto 列禁物。別の grid container 間で列幅を揃えたい場合、auto は内容次第で個別伸縮する。固定幅 (px) 指定するか、subgrid で track を共有する必要がある。

長文への防御 — 3 層の入力制限

初期の DB スキーマでは:

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

Postgres の text 型は実質無制限。10,000 文字の name でも保存できてしまい、UI レイアウトを破壊するリスクがあります。タブラーレイアウトに移行するなら、長文対策は必須。

採用した 3 層防御

  1. フロントエンド HTML5 制限: input に maxLength={60} / maxLength={512}
  2. サーバーサイド検証: Server Action で name.length > 60 を check してエラー返却
  3. 表示側 truncate: grid 各セルに truncate クラス。CSS Grid の minmax(0, ...) と組み合わせて長文を「…」省略

3 層目が特に重要。truncateoverflow: hidden + text-overflow: ellipsis + white-space: nowrap ですが、flex / grid 内では min-width: 0 が必要。minmax(0, ...) がこれを担保します。

デザイン反復は 16 ターン

今回のセッションで SiteCard のデザイン反復は 16 ターン。代表的なターン:

# 変更 理由
1 CopyBlock を light gray pill 化 黒地+緑文字が GitHub light で浮く
3 A/B/C/D 4 案比較 クリックアフォーダンス強化
4 1-col リストに転換 「装飾的すぎる」と却下されて構造から見直し
7 「ヒートマップを見る →」CTA 撤廃 「スタイリッシュじゃない」
9 CSS Grid タブラー化 セパレータベース文字列が「リストに見えない」
10 auto → 固定幅 ヘッダーとカードで列幅不一致
11 API KEY 列復活 「あれ、APIは?」
12 TRACKER TAG 列追加 「両方とも横並びで」
14 maxLength + サーバー検証 長文耐性の根本対応
16 見出し横に「+」ボタン (compact mode) 下スクロール不要のクイック追加

1 発で当たることはほぼ無い。デザイン反復は「dev server で実機表示 → 即時フィードバック → 次案」を 10+ 回繰り返す前提で計画する。Claude Code のような対話型 AI 開発環境がここで効く。

AIアシスト開発で効いたパターン

ツール固有の感想というより、対話型 AI と組むときに「これは効いた」と思った汎用パターン:

  • 新しい API 仕様の検索コストがゼロになる: Tailwind v4 の @custom-variant のような少し新しい構文を、即座に提案+動く形で出してくれる
  • 「実装→検証→修正」の閉ループが秒単位で回る: CSS Grid auto 列の不一致を再現→特定→修正まで 5 分
  • 同型の機械作業は一括で消化される: 15 言語ファイルの placeholder 置換 (in-golexample.com) を1ターンで
  • HMR との相性: 編集 → 保存 → ブラウザ即反映 → フィードバック → 再編集のフィードバックループを延々と回せる
  • 完成後の本番 push と Vercel デプロイ監視まで一気通貫

逆効果だったパターンは次節の「難しかった点」で。

難しかった点 / 次の改善

  • 「これ微妙」というレイヤーの抽象的なフィードバック を具体的な構造変更に翻訳するのが反復回数を増やした。最初に「タブラー or リスト or カード」レベルで合意を取るべきだった。
  • 10 ターン目あたりで「全部横並び」「全部 inline」「全部 truncate」と要件が増え続けたが、相反する制約 (情報密度 vs スキャナビリティ) があり最終形に到達するまで試行錯誤が必要だった。
  • モバイル対応はまだ手付かず。grid 7 列はスマホでは横スクロールが発生する。sm: media query で stack 化するか、別 component 分岐が必要。

まとめ

1 日のセッション内で、HeatMapX ダッシュボードを暗いカード grid から、ライト/ダーク 3 択 + GitHub 風タブラーリストに全面再設計しました。技術的なポイントは:

  1. Tailwind v4 の dark mode は @media ベースで開始、ユーザー切替が必要になったら @custom-variant + 同期スクリプトで拡張
  2. タブラーアラインメントは CSS Grid で 固定幅 (px)subgrid でしか実現できない
  3. 長文耐性は フロント maxLength + サーバー length check + 表示 truncate の 3 層防御
  4. デザイン反復は 10–16 ターン前提で「速いフィードバックループ」を構築する

仮説の答え合わせ(離脱率が実際に下がったか)は、2週間データを取った続編記事で公開予定です。

Claude Codeから動かすヒートマップを、まずは無料で。

計測タグを1行貼って、ブラウザ操作なしで分析・改善提案までCLIから受け取れます。クレカ不要・30秒でセットアップ。