Ein SaaS-Dashboard an einem Tag mit Claude Code neu aufbauen — Tailwind v4 + CSS Grid Tabular Alignment

HeatMapX Engineering Team14 min read
  • 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 Grid auto-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:

  1. Feste Breite: auto durch explizite Pixelwerte wie 110px, 72px ersetzen (was wir ausgeliefert haben)
  2. 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:

  1. Frontend HTML5-Limit: maxLength={60} / maxLength={512} auf Inputs
  2. Server-seitige Validierung: Server Action prüft name.length > 60 und gibt einen Fehler zurück
  3. Display-Truncation: truncate-Klasse auf jeder Grid-Zelle, kombiniert mit minmax(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-variant ist 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-golexample.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:

  1. 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.
  2. Tabellarische Ausrichtung mit CSS Grid: auto-Spalten fluchten nicht über separate Grid-Container. Feste Pixelbreiten oder CSS Subgrid sind die einzigen zuverlässigen Optionen.
  3. Robustheit gegen langen Text: Drei Schichten, alle erforderlich — Frontend maxLength, server-seitige Längenprüfung und Display truncate mit minmax(0, ...).
  4. 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.

Heatmaps aus Claude Code — kostenlos starten.

Ein Tracker-Tag einfügen, dann Analyse und CRO-Vorschläge aus der CLI bekommen.