Membangun Ulang Dashboard SaaS dalam Sehari dengan Claude Code — Tailwind v4 + CSS Grid Tabular Alignment
- engineering
- tailwind
- css-grid
- dark-mode
- claude-code
TL;DR
- Masalah: 11 dari 15 pengguna baru (73%) mendaftar namun pergi tanpa mendaftarkan satu pun URL
- Hipotesis: Setiap tools heatmap di kategori kami menggunakan UI light secara default — dashboard gelap kami mungkin membingungkan pengguna dan menyebabkan mereka pergi lebih awal
- Yang kami lakukan: Desain ulang dashboard penuh dalam satu hari — toggle light / dark / system + daftar tabular bergaya GitHub
- Pelajaran:
@custom-variantTailwind v4, jebakan kolomautopada CSS Grid, pertahanan 3 lapis melawan teks panjang, dan mengapa 10–16 iterasi desain adalah hal yang wajar, bukan pengecualian- Hasil: Dampak pada tingkat drop-off akan diukur; artikel lanjutan direncanakan setelah dua minggu pengumpulan data
Latar Belakang: Apakah Pengguna Pergi Karena Dashboard Kami Terlalu Gelap?
HeatMapX (heatmapx.com) adalah tools heatmap dan CRO yang bisa dipanggil dari Claude Code melalui CLI. Setelah memulai promosi pertama kami, kami menemukan sebuah pola: 11 dari 15 pengguna baru (73%) menyelesaikan proses daftar namun pergi tanpa mendaftarkan satu pun URL.
Satu hipotesis: setiap tools heatmap besar di kategori kami menggunakan UI light sebagai default. HeatMapX menjadi satu-satunya yang berbeda dengan antarmuka admin gelap. Pengguna baru yang mendarat di dashboard gelap di tengah lautan aplikasi bertema putih mungkin langsung pergi karena bingung.
Maka kami bekerja sama dengan Claude Code dan melakukan desain ulang penuh dalam satu sesi. Artikel ini membahas paruh pertama dari uji hipotesis tersebut — mendiagnosis drop-off dan mengeksekusi desain ulang. Kelanjutannya dengan angka nyata akan terbit setelah kami punya data dua minggu pasca-deploy.
Konfigurasi Desain Akhir
| Elemen | Light | Dark |
|---|---|---|
| Latar halaman | bg-neutral-100 (#f5f5f5) |
dark:bg-neutral-950 (#0a0a0a) |
| Permukaan kartu | bg-white + border-slate-200 |
dark:bg-slate-900 + dark:border-slate-800 |
| Teks utama | text-slate-900 |
dark:text-slate-100 |
| Warna utama (CTA) | bg-orange-600 (#ea580c) |
Digunakan di kedua mode |
| Border radius | rounded-md (6px) · tanpa bayangan · gaya GitHub |
|
| Toggle tema | Toggle tiga arah ☀ / 💻 / 🌙 di header |
Tailwind v4 Dark Mode — Dari Zero-Config hingga Toggle Manual
Mulai dengan @media prefers-color-scheme
Perilaku default Tailwind v4 memetakan varian dark: langsung ke @media (prefers-color-scheme: dark). Tanpa konfigurasi apapun, ia hanya mengikuti pengaturan dark mode sistem operasi.
{/* layout.tsx */}
<div className="bg-neutral-100 text-slate-900 dark:bg-neutral-950 dark:text-slate-100">
...
</div>
Ini mencakup kasus "ikuti sistem" (otomatis gelap di malam hari, dll.) tanpa pengaturan tambahan. Namun pengguna terkadang ingin mengganti pengaturan OS — "system" bisa salah, atau sekadar preferensi pribadi — sehingga kami memperluasnya menjadi toggle tiga arah.
Beralih ke varian dark berbasis class
Di Tailwind v4 Anda bisa mendefinisikan ulang perilaku varian dark: menggunakan @custom-variant. Kami beralih ke mode di mana dark: hanya aktif ketika .dark ada di <html>:
/* globals.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Pembungkus :where() menjaga spesifisitas di angka 0. Tanpanya, style varian dark bisa berkonflik dengan aturan spesifisitas yang sudah ada di stylesheet Anda.
Script sinkronisasi untuk mencegah SSR flash
Jika Anda menerapkan tema di dalam useEffect setelah hidrasi React, Anda akan mendapat flash-of-unstyled-content (FOUC) — kedipan putih singkat saat render pertama. Solusinya adalah script sinkronisasi inline yang disuntikkan ke <head> dan berjalan sebelum body dirender:
{/* 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>
Script ini berjalan secara sinkron sebelum React terpasang, sehingga .dark sudah ada di elemen <html> ketika body mulai dirender. Tambahkan suppressHydrationWarning ke <html> untuk menonaktifkan peringatan ketidakcocokan hidrasi.
💡 Pelajaran 1:
dark:Tailwind v4 bekerja tanpa konfigurasi. Jika pelacakan OS sudah cukup, Anda tidak perlu menyentuh apapun. Tambahkan@custom-variant+ script sinkronisasi hanya ketika Anda membutuhkan toggle manual dari pengguna — pendekatan dua tahap yang bersih.
CSS Grid Tabular Alignment — dan Di Mana Kolom auto Menjadi Jebakan
Daftar satu kolom awal tidak cukup
Setelah ada masukan bahwa kartu situs pasca-registrasi terasa "aneh," kami pertama kali mencoba hal yang paling jelas: mengganti grid dua kolom dengan daftar satu kolom. Setiap SiteCard menggunakan flexbox secara internal untuk menyejajarkan name · url · status · events, dengan chevron di kanan.
Masalah muncul segera setelah ada beberapa item:
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, dan › chevron semuanya berada di koordinat-x yang berbeda di setiap baris. Mata tidak bisa menemukan "kolom status ada di sini." Tampilannya tidak seperti daftar — melainkan teks yang ditumpuk secara vertikal.
Ini bukan daftar. Ini teks, yang ditumpuk secara vertikal.
Mengkonversi ke tata letak tabular CSS Grid
Kami mengganti bagian dalam flexbox dengan definisi kolom tetap CSS Grid. Karena setiap kartu berbagi template grid yang sama, tepi kiri kolom sejajar sempurna ke bawah halaman:
// 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>
Baris header menggunakan template grid yang persis sama:
// page.tsx header row
<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>
Jebakan: kolom auto di grid terpisah berukuran secara independen
Percobaan pertama kami menggunakan grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto]. Pemikirannya: "auto beradaptasi dengan konten, dan karena header dan SiteCard berbagi string template yang sama, mereka akan sejajar."
Ternyata tidak. Inilah alasannya:
- Kolom
autoheader: berukuran sesuai lebar teks "STATUS" - Kolom
autoSiteCard: berukuran sesuai lebar teks "● Active" - Keduanya adalah container grid terpisah — ukuran track dihitung secara independen untuk masing-masing
Dua solusi:
- Lebar tetap: Ganti
autodengan nilai pixel eksplisit seperti110px,72px(yang kami kirimkan) - CSS Subgrid: Letakkan header dan semua SiteCard dalam satu grid induk, lalu biarkan setiap baris mewarisi track induk via
subgrid(lebih kompleks)
💡 Pelajaran 2: Jangan gunakan kolom
autoketika Anda membutuhkan tabular alignment di antara container grid terpisah. Setiap container menghitung lebar tracknya sendiri berdasarkan kontennya sendiri. Gunakan lebar pixel tetap atau berbagi track via subgrid.
Pertahanan Terhadap Teks Panjang — 3 Lapis
Skema DB awal memiliki:
CREATE TABLE sites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL, -- tanpa batas panjang
url text NOT NULL, -- tanpa batas panjang
...
);
text di Postgres pada dasarnya tidak terbatas. Nama situs sepanjang 10.000 karakter adalah SQL yang valid secara teknis — dan akan langsung menghancurkan tata letak tabular. Beralih ke grid kolom tetap membuat pembatasan panjang teks menjadi tidak bisa ditawar.
Pertahanan 3 lapis yang kami terapkan:
- Batas HTML5 di frontend:
maxLength={60}/maxLength={512}pada input - Validasi sisi server: Server Action memeriksa
name.length > 60dan mengembalikan error - Pemenggalan tampilan: Class
truncatedi setiap sel grid, dikombinasikan denganminmax(0, ...)untuk mengaktifkan overflow
Lapis 3 adalah yang sering dilewatkan orang. truncate mengembang menjadi overflow: hidden + text-overflow: ellipsis + white-space: nowrap, namun dalam konteks flex atau grid juga membutuhkan min-width: 0 di sel — jika tidak, sel akan mengembang untuk muat kontennya dan overflow tidak pernah aktif. minmax(0, ...) menyediakan tepat itu.
16 Iterasi Desain
SiteCard saja melewati 16 iterasi desain dalam sesi ini. Beberapa iterasi yang dipilih:
| # | Perubahan | Alasan |
|---|---|---|
| 1 | Mengubah CopyBlock menjadi pil abu-abu terang | Latar hitam + teks hijau terlihat salah di GitHub light mode |
| 3 | Perbandingan empat opsi A/B/C/D | Membutuhkan affordance klik yang lebih kuat |
| 4 | Beralih ke daftar 1 kolom | Ditolak sebagai "terlalu dekoratif" — kembali ke dasar |
| 7 | Menghapus CTA "View Heatmap →" | "Tidak terlihat bersih" |
| 9 | Mengkonversi ke tata letak tabular CSS Grid | Tata letak berbasis pemisah "tidak terasa seperti daftar" |
| 10 | auto → lebar pixel tetap |
Header dan kolom kartu tidak sejajar |
| 11 | Memulihkan kolom API KEY | "Tunggu, di mana API key-nya?" |
| 12 | Menambah kolom TRACKER TAG | "Letakkan keduanya berdampingan" |
| 14 | Menambah maxLength + validasi server | Perbaikan proper untuk ketahanan terhadap teks panjang |
| 16 | Menambah tombol "+" di samping heading (mode kompak) | Tambah cepat tanpa scroll ke bawah |
⚠ Jangan berharap berhasil di percobaan pertama. Iterasi desain berarti "periksa di dev server → umpan balik langsung → percobaan berikutnya," diulang 10+ kali. Rencanakan loop tersebut. Lingkungan pengembangan AI interaktif seperti Claude Code adalah tempat pola ini benar-benar bersinar.
Pola yang Terbukti Efektif dengan Pengembangan Berbantuan AI
Ini bukan observasi spesifik tools — melainkan pola umum yang terbukti efektif ketika berpasangan dengan AI interaktif:
- Biaya lookup nol untuk sintaks API baru:
@custom-variantTailwind v4 relatif baru. Claude Code langsung menyajikannya dalam bentuk yang bisa langsung dipakai, tanpa perlu menggali dokumentasi. - Loop implement → verifikasi → perbaikan berjalan dalam detik: Mereproduksi misalignment
autoCSS Grid, mengisolasi penyebabnya, dan mengirimkan perbaikannya memakan waktu sekitar 5 menit total. - Pekerjaan massal mekanis menghilang: Mengganti teks placeholder di 15 file lokal (
in-gol→example.com) dalam satu iterasi. - Afinitas HMR: Edit → simpan → refresh browser instan → umpan balik → edit ulang. Loop berjalan cukup cepat sehingga Anda tetap dalam kondisi flow.
- End-to-end dalam satu sesi: Dari menyelesaikan desain ulang langsung hingga push ke produksi dan melihat deploy Vercel — tanpa pergantian konteks.
Pola yang kurang efektif ada di bagian berikutnya.
Apa yang Sulit / Apa yang Masih Tersisa
- Menerjemahkan umpan balik yang samar menjadi keputusan struktural memperbanyak jumlah iterasi. "Ini terasa aneh" harus menjadi "tabular vs. daftar vs. kartu" sebelum sesuatu yang berguna bisa terjadi. Mendapatkan kesepakatan struktural di awal bisa menghemat 3–4 iterasi.
- Sekitar iterasi ke-10, persyaratan terus berkembang — "semua inline," "semuanya dipotong," "semua kolom" — dan beberapa batasan benar-benar bertegangan (kepadatan informasi vs. kemudahan scan). Tidak ada jalan pintas yang bersih menuju bentuk akhir.
- Mobile belum disentuh. Grid 7 kolom menghasilkan scroll horizontal di layar kecil. Solusinya adalah media query
sm:yang menumpuk kolom, atau komponen mobile terpisah. Ini yang akan ditangani berikutnya.
Kesimpulan
Dalam satu sesi, kami membangun ulang dashboard HeatMapX dari grid kartu gelap menjadi tata letak toggle light/dark/system dengan daftar tabular bergaya GitHub. Poin teknis utama:
- Tailwind v4 dark mode: Mulai dengan dark mode berbasis
@media— ini zero-config. Tambahkan@custom-variant+ script sinkronisasi hanya ketika pengguna membutuhkan override manual. - Tabular alignment dengan CSS Grid: Kolom
autotidak sejajar di antara container grid terpisah. Gunakan lebar pixel tetap atau CSS Subgrid — keduanya adalah satu-satunya opsi yang dapat diandalkan. - Ketahanan terhadap teks panjang: Tiga lapis, semuanya diperlukan —
maxLengthdi frontend, pemeriksaan panjang sisi server, dantruncatetampilan denganminmax(0, ...). - Anggaran iterasi desain: Asumsikan 10–16 iterasi per komponen dan bangun loop umpan balik yang cepat sejak awal.
Apakah ini benar-benar menggerakkan tingkat drop-off masih menjadi pertanyaan terbuka. Kami akan menerbitkan lanjutannya dengan dua minggu data nyata.