Ein SaaS-Dashboard an einem Tag mit Claude Code neu aufbauen — Tailwind v4 + CSS Grid Tabular Alignment
- engineering
- tailwind
- css-grid
- dark-mode
- claude-code
TL;DR
- Problem: 11 von 15 neuen Nutzern (73 %) haben sich registriert, aber keine URL eingetragen
- Hypothese: Alle anderen Heatmap-Tools in unserer Kategorie setzen standardmäßig auf ein helles UI — unser dunkles Dashboard könnte Verwirrung auslösen und frühes Abspringen verursachen
- Was wir gemacht haben: Vollständiges Dashboard-Redesign an einem Tag — Light / Dark / System-Toggle + GitHub-artiger tabellarischer Listenansicht
- Erkenntnisse: Tailwind v4
@custom-variant, die CSS Gridauto-Spalten-Falle, eine 3-lagige Verteidigung gegen langen Text und warum 10–16 Design-Iterationen die Norm sind, nicht die Ausnahme- Ergebnis: Auswirkung auf die Absprungrate wird noch gemessen; Follow-up-Post geplant nach zwei Wochen mit Daten
Hintergrund: Verloren wir Nutzer, weil unser Dashboard zu dunkel war?
HeatMapX (heatmapx.com) ist ein Heatmap- und CRO-Tool, das sich über CLI direkt aus Claude Code aufrufen lässt. Nach unserem ersten Promotion-Push fiel uns ein Muster auf: 11 von 15 neuen Nutzern (73 %) haben die Registrierung abgeschlossen, aber keine einzige URL eingetragen.
Eine Hypothese: Jedes etablierte Heatmap-Tool in unserer Kategorie liefert standardmäßig ein helles UI. HeatMapX war das Ausreißer mit einem dunklen Admin-Interface. Erstnutzer, die in einer Welt weiß gestalteter Apps auf ein dunkles Dashboard treffen, springen womöglich einfach aus Verwirrung ab.
Also haben wir uns mit Claude Code zusammengetan und in einer einzigen Session ein vollständiges Redesign durchgezogen. Dieser Post deckt die erste Hälfte dieses Hypothesentests ab — die Diagnose des Absprungs und die Durchführung des Redesigns. Der Follow-up mit echten Zahlen kommt, sobald wir zwei Wochen Post-Deploy-Daten haben.
Die finale Design-Konfiguration
| Element | Light | Dark |
|---|---|---|
| Seitenhintergrund | bg-neutral-100 (#f5f5f5) |
dark:bg-neutral-950 (#0a0a0a) |
| Kartenoberfläche | bg-white + border-slate-200 |
dark:bg-slate-900 + dark:border-slate-800 |
| Primärer Text | text-slate-900 |
dark:text-slate-100 |
| Akzentfarbe (CTA) | bg-orange-600 (#ea580c) |
In beiden Modi identisch |
| Border-Radius | rounded-md (6px) · keine Schatten · GitHub-Stil |
|
| Theme-Toggle | ☀ / 💻 / 🌙 Dreiwege-Toggle im Header |
Tailwind v4 Dark Mode — Von Zero-Config zum manuellen Toggle
Start mit @media prefers-color-scheme
Tailwinds v4-Standardverhalten mappt die dark:-Variante direkt auf @media (prefers-color-scheme: dark). Out of the Box, ohne jegliche Konfiguration, folgt es der OS-Dark-Mode-Einstellung.
{/* layout.tsx */}
<div className="bg-neutral-100 text-slate-900 dark:bg-neutral-950 dark:text-slate-100">
...
</div>
Das deckt den „System-folgt"-Fall (automatisch dunkel nachts usw.) ohne Extra-Setup ab. Aber manchmal wollen Nutzer die OS-Einstellung überschreiben — „system" kann daneben liegen, oder es ist einfach persönliche Präferenz — daher haben wir es zu einem Dreiwege-Toggle erweitert.
Wechsel zur klassenbasierten Dark-Variante
In Tailwind v4 kann man das dark:-Variantenverhalten mit @custom-variant umdefinieren. Wir haben auf einen Modus gewechselt, bei dem dark: nur aktiv wird, wenn .dark auf <html> vorhanden ist:
/* globals.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Der :where()-Wrapper hält die Spezifität bei 0. Ohne ihn können die Dark-Varianten-Styles mit bestehenden Spezifitätsregeln im Stylesheet kollidieren.
Sync-Skript gegen SSR-Flash
Wenn das Theme erst innerhalb von useEffect nach der React-Hydration angewendet wird, entsteht ein Flash-of-Unstyled-Content (FOUC) — ein kurzes weißes Aufflackern beim ersten Render. Die Lösung ist ein inline Sync-Skript, das in den <head> injiziert wird und vor dem Body-Rendering ausgeführt wird:
{/* 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>
Das läuft synchron, bevor React mountet — .dark ist also bereits auf <html>, wenn der Body zu rendern beginnt. suppressHydrationWarning zu <html> hinzufügen, um die Hydration-Mismatch-Warnung zu unterdrücken.
💡 Erkenntnis 1: Tailwinds v4
dark:funktioniert zero-config. Wenn OS-Tracking ausreicht, muss man gar nichts anfassen.@custom-variant+ das Sync-Skript kommen nur dazu, wenn ein manueller Nutzer-Toggle explizit gebraucht wird — ein sauberer zweistufiger Ansatz.
CSS Grid Tabular Alignment — Wo auto-Spalten beißen
Die initiale Einzelspaltenliste reichte nicht aus
Nach dem Hinweis, dass die Sitekarten nach der Registrierung „irgendwie komisch" wirkten, probierten wir zunächst das Naheliegende: ein zweispaltiges Karten-Grid gegen eine einspältige Liste getauscht. Jede SiteCard verwendete intern Flexbox, um name · url · status · events auszurichten, mit einem Chevron rechts.
Das Problem zeigte sich sofort mit mehreren Einträgen:
Kurz · example.com · ● Active · 5.577
Sehr langer Seitenname... · normal.com · ● Active · 5.577
ACME · acme.io · ● Active · 5.577
· Active, · 5.577 und › Chevron landen bei jeder Zeile an anderen x-Koordinaten. Das Auge kann sich nicht verankern — es scannt nicht wie eine Liste, sondern liest sich wie gestapelter Fließtext.
Das war keine Liste. Das war Text, vertikal gestapelt.
Umstellung auf ein CSS Grid Tabular Layout
Die Flexbox-Internals wurden durch eine CSS Grid Fixed-Column-Definition ersetzt. Da alle Karten dasselbe Grid-Template teilen, fluchten die linken Kanten aller Spalten perfekt über die ganze Seite:
// 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>
Die Header-Zeile verwendet exakt dasselbe Grid-Template:
// page.tsx Header-Zeile
<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>
Die Falle: auto-Spalten in separaten Grids werden unabhängig dimensioniert
Im ersten Anlauf verwendeten wir grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto]. Die Überlegung: „auto passt sich dem Inhalt an, und da Header und SiteCard denselben Template-String teilen, werden sie fluchten."
Haben sie nicht. Der Grund:
- Header
auto-Spalte: dimensioniert auf die Textbreite von „STATUS" - SiteCard
auto-Spalte: dimensioniert auf die Textbreite von „● Active" - Beide sind separate Grid-Container — Track-Sizing wird für jeden unabhängig berechnet
Zwei Lösungen:
- Feste Breite:
autodurch explizite Pixelwerte wie110px,72pxersetzen (was wir ausgeliefert haben) - CSS Subgrid: Header und alle SiteCards in einem gemeinsamen Eltern-Grid, dann erbt jede Zeile die Tracks des Eltern via
subgrid(komplexer)
💡 Erkenntnis 2: Keine
auto-Spalten verwenden, wenn tabellarische Ausrichtung über separate Grid-Container hinweg gebraucht wird. Jeder Container berechnet seine Track-Breiten auf Basis seines eigenen Inhalts. Feste Pixelbreiten oder Subgrid sind die einzigen zuverlässigen Optionen.
Verteidigung gegen langen Text — 3 Schichten
Das ursprüngliche DB-Schema hatte:
CREATE TABLE sites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL, -- kein Längenlimit
url text NOT NULL, -- kein Längenlimit
...
);
Postgres text ist effektiv unbegrenzt. Ein 10.000-Zeichen-Seitenname ist vollkommen valides SQL — und würde ein tabellarisches Layout sofort zerstören. Der Wechsel zu einem Fixed-Column-Grid machte Textlängen-Constraints unverhandelbar.
Die 3-Schichten-Verteidigung, bei der wir gelandet sind:
- Frontend HTML5-Limit:
maxLength={60}/maxLength={512}auf Inputs - Server-seitige Validierung: Server Action prüft
name.length > 60und gibt einen Fehler zurück - Display-Truncation:
truncate-Klasse auf jeder Grid-Zelle, kombiniert mitminmax(0, ...)um den Overflow zu ermöglichen
Schicht 3 ist die, die die meisten übersehen. truncate expandiert zu overflow: hidden + text-overflow: ellipsis + white-space: nowrap, braucht aber in einem Flex- oder Grid-Kontext auch min-width: 0 auf der Zelle — sonst expandiert die Zelle auf ihren Inhalt und Overflow wird nie ausgelöst. minmax(0, ...) liefert genau das.
16 Design-Iterationen
Die SiteCard allein durchlief in dieser Session 16 Design-Iterationen. Ausgewählte Runden:
| # | Änderung | Grund |
|---|---|---|
| 1 | CopyBlock auf hellgraue Pill umgestellt | Schwarzer Hintergrund + grüner Text wirkte im GitHub-Light-Mode falsch |
| 3 | A/B/C/D Vier-Optionen-Vergleich | Stärkeres Click-Affordance gebraucht |
| 4 | Auf 1-Spalten-Liste gewechselt | Als „zu dekorativ" abgelehnt — zurück zu den Basics |
| 7 | „View Heatmap →"-CTA entfernt | „Wirkt nicht sauber" |
| 9 | Auf CSS Grid Tabular Layout umgestellt | Trennzeichen-basiertes String-Layout „fühlt sich nicht wie eine Liste an" |
| 10 | auto → feste Pixelbreiten |
Header- und Kartenspalten fluchten nicht |
| 11 | API KEY-Spalte wiederhergestellt | „Warte, wo ist der API Key?" |
| 12 | TRACKER TAG-Spalte hinzugefügt | „Stell beide nebeneinander" |
| 14 | maxLength + Server-Validierung hinzugefügt | Saubere Lösung für Robustheit bei langem Text |
| 16 | „+"-Button neben der Überschrift hinzugefügt (Compact Mode) | Schnelles Hinzufügen ohne nach unten zu scrollen |
⚠ Nicht erwarten, es beim ersten Versuch zu treffen. Design-Iteration bedeutet „im Dev-Server prüfen → sofortiges Feedback → nächster Versuch", 10+ Mal wiederholt. Diesen Loop einplanen. Eine interaktive KI-Entwicklungsumgebung wie Claude Code ist der Ort, wo dieses Muster wirklich glänzt.
Muster, die mit KI-unterstützter Entwicklung tatsächlich funktioniert haben
Das sind keine werkzeugspezifischen Beobachtungen — es sind allgemeine Muster, die sich beim Pairing mit einer interaktiven KI als effektiv erwiesen haben:
- Null Lookup-Kosten für neue API-Syntax: Tailwind v4s
@custom-variantist relativ neu. Claude Code lieferte es sofort in funktionierender Form, ohne Docs-Suche. - Der Implement → Verify → Fix-Loop läuft in Sekunden: Die CSS Grid
auto-Fehausrichtung reproduzieren, die Ursache isolieren und den Fix ausliefern — das dauerte insgesamt etwa 5 Minuten. - Mechanische Massenarbeit verschwindet: Placeholder-Text in 15 Locale-Dateien ersetzen (
in-gol→example.com) in einer einzigen Iteration. - HMR-Affinität: Bearbeiten → Speichern → sofortige Browser-Aktualisierung → Feedback → Neu-Bearbeiten. Der Loop läuft schnell genug, um im Flow-State zu bleiben.
- End-to-End in einer Session: Vom abgeschlossenen Redesign direkt bis zum Production-Push und dem Beobachten des Vercel-Deploys — kein Context-Switching.
Die Muster, die weniger gut funktioniert haben, sind im nächsten Abschnitt.
Was schwierig war / Was noch aussteht
- Vages Feedback in strukturelle Entscheidungen übersetzen hat die Iterationsanzahl multipliziert. „Das fühlt sich irgendwie falsch an" musste erst zu „tabellarisch vs. Liste vs. Karten" werden, bevor etwas Sinnvolles passieren konnte. Diese strukturelle Einigung vorab zu treffen hätte 3–4 Iterationen gespart.
- Rund um Iteration 10 haben sich die Anforderungen immer weiter ausgedehnt — „alles inline", „alles gekürzt", „alle Spalten" — und manche Constraints standen echten Spannungen gegenüber (Informationsdichte vs. Scannbarkeit). Es gab keinen sauberen Abkürzungsweg zur finalen Form.
- Mobile ist unangetastet. Ein 7-Spalten-Grid erzeugt horizontales Scrollen auf kleinen Bildschirmen. Die Lösung ist entweder eine
sm:-Media-Query, die Spalten stapelt, oder eine separate Mobile-Komponente. Das ist das nächste, was angegangen wird.
Fazit
In einer einzigen Session haben wir das HeatMapX-Dashboard von einem dunklen Karten-Grid zu einem Light/Dark/System-Toggle-Layout mit einer GitHub-artigen tabellarischen Liste neu aufgebaut. Die wesentlichen technischen Erkenntnisse:
- Tailwind v4 Dark Mode: Mit
@media-basiertem Dark Mode starten — er ist zero-config.@custom-variant+ das Sync-Skript nur hinzufügen, wenn Nutzer einen manuellen Override brauchen. - Tabellarische Ausrichtung mit CSS Grid:
auto-Spalten fluchten nicht über separate Grid-Container. Feste Pixelbreiten oder CSS Subgrid sind die einzigen zuverlässigen Optionen. - Robustheit gegen langen Text: Drei Schichten, alle erforderlich — Frontend
maxLength, server-seitige Längenprüfung und Displaytruncatemitminmax(0, ...). - Design-Iterationsbudget: 10–16 Iterationen pro Komponente einplanen und von Anfang an einen schnellen Feedback-Loop aufbauen.
Ob das die Absprungrate tatsächlich bewegt, ist noch eine offene Frage. Der Follow-up mit zwei Wochen echter Daten kommt.