Moderne Dashboards mit Tailwind CSS und TypeScript gestalten
Muster für Admin-Dashboards, die auch in großem Maßstab wartbar bleiben — typisierte Datenverträge, komponierbare Tailwind-Bausteine, semantische Farbe und eine klare Grenze zwischen Server- und UI-State.
Mehran Hatami6 Min. LesezeitDashboards scheitern auf vorhersehbare Weise: Aus wenigen Bildschirmen werden Dutzende, State verheddert sich zwischen Komponenten, und niemand weiß mehr, welche Prop was steuert. Das ist eigentlich kein Tailwind- oder TypeScript-Problem — es ist ein Architekturproblem, das sich nur zufällig im Markup und in den Typen zeigt, weil es dort am leichtesten zu erkennen ist.
Im Folgenden die Muster, die ich tatsächlich einsetze, damit ein Dashboard weit über den Punkt hinaus wartbar bleibt, an dem die meisten zu verrotten beginnen: ein typisierter Datenvertrag vor jeder Komponente, ein kleiner Satz komponierter Bausteine statt kopierter Karten, Farbe als Signal statt Dekoration, und eine harte Grenze zwischen Server-State und UI-State.
Mit dem Datenvertrag beginnen, nicht mit der Komponente
Eine klare Grenze zwischen Server-State und UI-State entscheidet maßgeblich darüber, ob ein großes Dashboard wartbar bleibt.
Jeder Dashboard-Bildschirm rendert etwas Konkretes — Aufträge, Lizenzen, Lagerbestand, Umsatz. Bevor ich auch nur ein div schreibe, schreibe ich den TypeScript-Typ, der dieses Konkrete beschreibt, geteilt zwischen der API-Schicht und der Komponente, die ihn rendert. Sobald dieser Typ existiert, schrumpft die Aufgabe der Komponente auf „diese Form rendern“ — ein viel einfacheres Problem als „herausfinden, welche Daten hier auftauchen könnten“.
Ich validiere an der Grenze — meist mit zod, genau dort, wo Daten über eine API-Route oder eine Server Action in die App gelangen — und vertraue danach dem abgeleiteten Typ überall. Dieser eine Validierungsaufruf ist mehr wert als defensives Optional-Chaining, verstreut über vierzig Komponenten.
Eine Typo-Skala, die dichte Daten übersteht
Dashboards packen mehr Informationen pro Bildschirm als fast jede andere UI-Kategorie, daher wird eine Typo-Skala, die für eine Marketingseite funktioniert, ständig gegen Sie arbeiten. Ich halte sie bewusst klein und binde jede Stufe an eine konkrete Aufgabe, nicht an ein Gefühl:
- 12px / text-xs — Metadaten in Tabellenzellen: Zeitstempel, IDs, sekundäre Labels
- 14px / text-sm — Fließtext, Formularlabels, Standard für die meisten Tabellenzellen
- 16px / text-base — der primäre Datenwert in einer Tabellenzeile, auf einen Blick erfassbar
- 20px / text-xl — Zahlen in Stat-Kacheln, sparsam eingesetzt, damit sie weiterhin den Blick lenken
- 28px aufwärts — nur Seitenüberschriften, einmal pro Bildschirm, nie innerhalb einer Karte
Die Regel, die mehr zählt als die genauen Pixelwerte: Sobald einer Größe eine Aufgabe zugewiesen ist, darf nichts anderes sie für eine andere Aufgabe wiederverwenden, nur weil sie an einer Stelle zufällig richtig aussieht.
Bausteine komponieren statt Karten kopieren
Tailwind CSS funktioniert in diesem Kontext am besten, wenn es komponiert statt kopiert wird. Ich baue einen kleinen Satz an Grundbausteinen — Card, Stat-Kachel, Tabellenzeile — einmal, mit Varianten über einfache Props, sodass ein Dashboard mit fünfzig Bildschirmen eine visuelle Sprache hat statt fünfzig leicht unterschiedlicher. Eine Stat-Kachel sieht am Ende so aus:
interface StatTileProps {
label: string;
value: string | number;
tone?: "neutral" | "success" | "warning" | "danger";
}
export function StatTile({ label, value, tone = "neutral" }: StatTileProps) {
return (
<div className="glass-card rounded-card p-4">
<p className="text-xs text-foreground/50">{label}</p>
<p className={cn("mt-1 text-xl font-semibold", toneText[tone])}>
{value}
</p>
</div>
);
}Jeder Bildschirm, der eine Zahl in einer Box braucht, verwendet dieselbe eine Komponente wieder. Wenn sich das Design ändert, ändert es sich an einer Stelle — nicht in den vierzig Bildschirmen, die das ursprüngliche Markup kopiert haben, bevor jemand das Muster bemerkt hat.
Farbe als Signal, nicht als Dekoration
Die Markenfarbe eines Dashboards und seine semantischen Farben (Erfolg, Warnung, Gefahr) sollten niemals dasselbe Token sein. Ist Ihre Markenfarbe zufällig Grün, verliert ein „Erfolg“-Zustand, der sie wiederverwendet, in dem Moment seine Bedeutung, in dem irgendwo ein grüner Button gewünscht wird, der nichts damit zu tun hat. Ich halte eine kleine, separate semantische Palette — nur für Zustände, nie fürs Branding — damit ein rotes Badge immer „braucht Aufmerksamkeit“ bedeutet und nichts sonst mit diesem Signal konkurriert.
Die Grenze zwischen Server-State und UI-State
State-Grenzen sind genauso wichtig wie Styling. Server-State — Aufträge, Nutzer, Lizenzen, alles, was aus einer Datenbank stammt — gehört in Server Components oder eine dedizierte Datenschicht. UI-State — ein geöffnetes Dropdown, ein ausgewählter Tab, ein Formular-Entwurf — bleibt lokal in der Komponente, die ihn besitzt, und wird nie nur aus Bequemlichkeit zu globalem State befördert.
Das Vermischen beider ist der häufigste Grund, warum Dashboards schwer nachvollziehbar werden: Ein useState, das still und leise zur Wahrheitsquelle für etwas geworden ist, das der Server ebenfalls zu besitzen glaubt, ist die Quelle der meisten wirklich verwirrenden Bugs.
Performance: das Dashboard, das nicht ruckelt
Dichte Bildschirme bestrafen naives Rendering. Eine Tabelle mit 2.000 Zeilen, die bei jedem Tastenanschlag in einem Filterfeld neu rendert, oder eine Seite, die blockiert, bis die langsamste von sechs Datenquellen geantwortet hat, wirken beide „langsam“ — selbst bei schneller Verbindung. Drei Dinge beheben das meiste: lange Listen virtualisieren, sodass nur sichtbare Zeilen gemountet werden, abgeleitete Werte (Summen, sortierte Ansichten) memoisieren statt bei jedem Render neu zu berechnen, und langsamere Abschnitte mit Suspense-Grenzen streamen, damit der Rest der Seite nicht von einem länger ladenden Bericht als Geisel gehalten wird.
Eine Checkliste vor dem Ausliefern eines neuen Bildschirms
- Hat die Daten einen gemeinsamen TypeScript-Typ, einmal an der Grenze validiert?
- Verwendet jede Karte, Kachel und Tabellenzeile einen bestehenden Baustein statt neues Markup?
- Wird Farbe nur dort eingesetzt, wo sie ein Signal ist — nie als reine Dekoration?
- Gibt es für jeden State einen klaren Besitzer: Server oder lokale UI?
- Bleibt eine lange Tabelle bei echten Produktionsdatenmengen flüssig, nicht nur mit zehn Testzeilen?
- Ergibt die Seite mit prefers-reduced-motion und ganz ohne Animation noch Sinn?
- Könnte ein neues Teammitglied eine weitere Metrik-Kachel hinzufügen, ohne ein neues Muster zu erfinden?
Der Gewinn aus all dem ist ein Dashboard, das sich auch ein Jahr später noch angenehm erweitern lässt: Eine neue Kennzahl oder Tabelle bedeutet, bestehende Bausteine und Typen wiederzuverwenden, nicht jedes Mal ein neues Muster zu erfinden — genau der Punkt, an dem die meisten Dashboards dieser Größe bereits zu verrotten begonnen haben.