Refonte complète d'un tableau de bord SaaS en une journée avec Claude Code — Tailwind v4 + alignement tabulaire CSS Grid

HeatMapX Engineering Team16 min read
  • engineering
  • tailwind
  • css-grid
  • dark-mode
  • claude-code

TL;DR

  • Problème : 11 nouveaux utilisateurs sur 15 (73 %) se sont inscrits puis ont quitté l'application sans enregistrer d'URL
  • Hypothèse : tous les outils de heatmap de notre catégorie proposent une interface claire par défaut — notre tableau de bord sombre semblait être à l'origine de la confusion et d'un abandon précoce
  • Ce que nous avons fait : refonte complète du tableau de bord en une journée — bascule light / dark / système + liste tabulaire façon GitHub
  • Apprentissages : @custom-variant de Tailwind v4, le piège de la colonne auto en CSS Grid, une défense à 3 couches contre les textes longs, et pourquoi 10 à 16 itérations de design est la norme, pas l'exception
  • Résultat : impact sur le taux d'abandon à mesurer ; un article de suivi est prévu après deux semaines de données

Contexte : perdions-nous des utilisateurs parce que notre tableau de bord était trop sombre ?

HeatMapX (heatmapx.com) est un outil de heatmap et de CRO que vous pouvez appeler depuis Claude Code via CLI. Après le lancement de notre première campagne promotionnelle, nous avons observé un schéma récurrent : 11 des 15 nouveaux utilisateurs (73 %) ont finalisé leur inscription, mais sont repartis sans enregistrer une seule URL.

Une hypothèse s'est imposée : tous les grands outils de heatmap de notre catégorie proposent une interface claire par défaut. HeatMapX faisait figure d'exception avec son interface d'administration sombre. Les nouveaux utilisateurs atterrissant sur un tableau de bord sombre, au milieu d'applications aux thèmes clairs, partaient peut-être simplement par désorientation.

Nous avons donc travaillé avec Claude Code et effectué une refonte complète en une seule session. Cet article couvre la première partie de ce test d'hypothèse — le diagnostic de l'abandon et l'exécution de la refonte. Le suivi avec les chiffres réels viendra une fois que nous aurons deux semaines de données post-déploiement.

La configuration de design finale

Élément Clair Sombre
Arrière-plan de la page bg-neutral-100 (#f5f5f5) dark:bg-neutral-950 (#0a0a0a)
Surface des cartes bg-white + border-slate-200 dark:bg-slate-900 + dark:border-slate-800
Texte principal text-slate-900 dark:text-slate-100
Couleur clé (CTA) bg-orange-600 (#ea580c) Partagée entre les deux modes
Border radius rounded-md (6px) · sans ombres · style GitHub
Bascule de thème ☀ / 💻 / 🌙 bascule à trois positions dans l'en-tête

Mode sombre Tailwind v4 — Du zéro-config au basculement manuel

Débuter avec @media prefers-color-scheme

Le comportement par défaut de Tailwind v4 associe la variante dark: directement à @media (prefers-color-scheme: dark). Sans aucune configuration, il suit simplement le paramètre de mode sombre du système d'exploitation.

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

Cela couvre le cas « suivre le système d'exploitation » (sombre automatique la nuit, etc.) sans configuration supplémentaire. Mais les utilisateurs veulent parfois contourner le réglage du système — le mode « auto » peut mal fonctionner, ou c'est une simple préférence personnelle — nous avons donc étendu cela à une bascule à trois positions.

Passer à la variante dark basée sur une classe CSS

Dans Tailwind v4, il est possible de redéfinir le comportement de la variante dark: avec @custom-variant. Nous sommes passés à un mode où dark: ne se déclenche que lorsque .dark est présent sur <html> :

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

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

L'enveloppe :where() maintient la spécificité à 0. Sans elle, les styles de la variante dark peuvent entrer en conflit avec les règles de spécificité existantes ailleurs dans votre feuille de styles.

Script de synchronisation pour éviter le flash au chargement

Si vous appliquez le thème dans useEffect après l'hydratation de React, vous obtenez un flash of unstyled content (FOUC) — un bref scintillement blanc au premier rendu. La solution consiste à injecter un script de synchronisation inline dans <head> qui s'exécute avant le rendu du corps :

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

Ce script s'exécute de manière synchrone avant le montage de React, de sorte que .dark est déjà présent sur l'élément <html> au moment où le corps commence à se rendre. Ajoutez suppressHydrationWarning à <html> pour taire l'avertissement de divergence d'hydratation.

💡 Apprentissage 1 : La variante dark: de Tailwind v4 fonctionne sans configuration. Si le suivi du système d'exploitation vous suffit, vous n'avez rien à toucher. N'ajoutez @custom-variant + le script de synchronisation que si vous avez explicitement besoin d'une bascule manuelle — une approche propre en deux étapes.

Alignement tabulaire CSS Grid — et où les colonnes auto vous piègent

La liste initiale en une colonne ne suffisait pas

Après un retour indiquant que les cartes de sites post-inscription « ne semblaient pas justes », nous avons d'abord essayé la solution évidente : remplacer une grille de cartes à deux colonnes par une liste à une colonne. Chaque SiteCard utilisait flexbox en interne pour aligner nom · URL · statut · événements, avec un chevron à droite.

Le problème est apparu dès qu'il y avait plusieurs éléments :

Court · example.com · ● Actif · 5 577
Nom de site très long qui... · normal.com · ● Actif · 5 577
ACME · acme.io · ● Actif · 5 577

· Actif, · 5 577 et › chevron atterrissaient tous à des coordonnées x différentes sur chaque ligne. L'œil ne peut pas s'ancrer sur « la colonne statut est ici ». Ça ne ressemble pas à une liste — on dirait des chaînes de texte empilées.

Ce n'était pas une liste. C'était du texte, empilé verticalement.

Conversion vers une mise en page tabulaire CSS Grid

Nous avons remplacé les éléments internes en flexbox par une définition de colonnes fixes en CSS Grid. Parce que chaque carte partage le même gabarit de grille, les bords gauches des colonnes s'alignent parfaitement sur toute la page :

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

La ligne d'en-tête utilise exactement le même gabarit de grille :

// page.tsx — ligne d'en-tête
<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>CLÉ API</span>
  <span>TAG TRACKER</span>
  <span>STATUT</span>
  <span>AUJOURD'HUI</span>
  <span></span>
</div>

Le piège : les colonnes auto dans des grilles séparées se dimensionnent indépendamment

Notre premier essai utilisait grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto_auto_auto]. L'idée : « auto s'adapte au contenu, et comme l'en-tête et le SiteCard partagent le même gabarit, ils s'aligneront. »

Ce n'était pas le cas. Voici pourquoi :

  • Colonne auto de l'en-tête : dimensionnée sur la largeur du texte « STATUT »
  • Colonne auto du SiteCard : dimensionnée sur la largeur du texte « ● Actif »
  • Ce sont deux conteneurs de grille séparés — le dimensionnement des pistes est calculé indépendamment pour chacun

Deux solutions :

  1. Largeur fixe : remplacer auto par des valeurs en pixels explicites comme 110px, 72px (ce que nous avons livré)
  2. CSS Subgrid : placer l'en-tête et tous les SiteCards dans une grille parente unique, puis faire hériter à chaque ligne les pistes du parent via subgrid (plus complexe)

💡 Apprentissage 2 : N'utilisez pas de colonnes auto lorsque vous avez besoin d'un alignement tabulaire entre des conteneurs de grille séparés. Chaque conteneur calcule ses propres largeurs de pistes en fonction de son propre contenu. Utilisez des largeurs en pixels fixes ou partagez les pistes via subgrid.

Se défendre contre les textes longs — 3 couches

Le schéma initial de la base de données était :

CREATE TABLE sites (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,  -- sans limite de longueur
  url text NOT NULL,   -- sans limite de longueur
  ...
);

Le type text de Postgres est pratiquement illimité. Un nom de site de 10 000 caractères est du SQL parfaitement valide — et détruirait instantanément une mise en page tabulaire. Le passage à une grille à colonnes fixes rendait les contraintes de longueur de texte incontournables.

La défense à 3 couches que nous avons retenue :

  1. Limite HTML5 côté client : maxLength={60} / maxLength={512} sur les champs de saisie
  2. Validation côté serveur : la Server Action vérifie name.length > 60 et renvoie une erreur
  3. Troncature à l'affichage : classe truncate sur chaque cellule de la grille, combinée avec minmax(0, ...) pour permettre le débordement

La couche 3 est celle que les gens oublient. truncate se décompose en overflow: hidden + text-overflow: ellipsis + white-space: nowrap, mais dans un contexte flex ou grid, la cellule a aussi besoin de min-width: 0 — sans cela, la cellule s'étend pour s'adapter à son contenu et le débordement ne se déclenche jamais. minmax(0, ...) fournit exactement cela.

16 itérations de design

Le SiteCard seul a traversé 16 itérations de design au cours de cette session. Sélection de tours marquants :

# Modification Raison
1 Conversion de CopyBlock en pastille gris clair Fond noir + texte vert ne convenait pas en mode clair façon GitHub
3 Comparaison de quatre options A/B/C/D Besoin d'une affordance de clic plus forte
4 Passage à une liste en 1 colonne Rejeté comme « trop décoratif » — retour aux bases
7 Suppression du CTA « Voir le heatmap → » « Ça ne semble pas propre »
9 Conversion en mise en page tabulaire CSS Grid La mise en page basée sur des séparateurs « ne ressemble pas à une liste »
10 auto → largeurs en pixels fixes Les colonnes de l'en-tête et de la carte ne s'alignaient pas
11 Restauration de la colonne CLÉ API « Attends, où est la clé API ? »
12 Ajout de la colonne TAG TRACKER « Mets les deux côte à côte »
14 Ajout de maxLength + validation serveur Correction correcte pour la robustesse face aux textes longs
16 Ajout d'un bouton « + » à côté de l'en-tête (mode compact) Ajout rapide sans avoir à défiler

Ne vous attendez pas à réussir du premier coup. Itérer sur le design signifie « vérifier sur le serveur de développement → retour immédiat → prochaine tentative », répété 10 fois ou plus. Prévoyez cette boucle. Un environnement de développement IA interactif comme Claude Code est là où ce schéma brille vraiment.

Schémas qui ont réellement fonctionné avec le développement assisté par IA

Il ne s'agit pas d'observations spécifiques à un outil — ce sont des schémas généraux qui se sont révélés efficaces lorsqu'on collabore avec une IA interactive :

  • Zéro coût de recherche de nouvelle syntaxe API : le @custom-variant de Tailwind v4 est relativement récent. Claude Code l'a fourni immédiatement sous forme fonctionnelle, sans avoir à fouiller la documentation.
  • La boucle implémenter → vérifier → corriger s'exécute en secondes : reproduire le défaut d'alignement de la colonne auto en CSS Grid, en isoler la cause et livrer le correctif a pris environ 5 minutes au total.
  • Les tâches mécaniques répétitives disparaissent : remplacer le texte de substitution dans 15 fichiers de locale (in-golexample.com) en une seule itération.
  • Affinité avec le rechargement à chaud (HMR) : édition → sauvegarde → actualisation instantanée du navigateur → retour → nouvelle édition. La boucle est suffisamment rapide pour rester en état de flux.
  • De bout en bout en une seule session : depuis la fin de la refonte jusqu'au déploiement en production et la surveillance du déploiement Vercel — sans changement de contexte.

Les schémas qui ont moins bien fonctionné sont dans la section suivante.

Ce qui était difficile / Ce qui reste à faire

  • Traduire un retour vague en décisions structurelles a multiplié le nombre d'itérations. « Ça semble bizarre » devait devenir « tabulaire vs. liste vs. cartes » avant que quoi que ce soit d'utile puisse se passer. S'entendre sur la structure en amont aurait économisé 3 à 4 itérations.
  • Vers l'itération 10, les exigences n'arrêtaient pas de s'élargir — « tout en ligne », « tout tronqué », « toutes les colonnes » — et certaines contraintes étaient en tension réelle (densité de l'information vs. lisibilité). Il n'y avait pas de raccourci propre vers la forme finale.
  • Le mobile n'a pas été touché. Une grille à 7 colonnes produit un défilement horizontal sur les petits écrans. La solution passe soit par une requête média sm: qui empile les colonnes, soit par un composant mobile séparé. C'est la prochaine chose à traiter.

Conclusion

En une seule session, nous avons reconstruit le tableau de bord HeatMapX d'une grille de cartes sombre vers une mise en page avec bascule light/dark/système et une liste tabulaire façon GitHub. Les points techniques clés :

  1. Mode sombre Tailwind v4 : commencez avec le mode sombre basé sur @media — c'est sans configuration. N'ajoutez @custom-variant + le script de synchronisation que lorsque les utilisateurs ont besoin d'un remplacement manuel.
  2. Alignement tabulaire avec CSS Grid : les colonnes auto ne s'alignent pas entre des conteneurs de grille séparés. Utilisez des largeurs en pixels fixes ou CSS Subgrid — ce sont les seules options fiables.
  3. Robustesse face aux textes longs : trois couches, toutes obligatoires — maxLength côté client, vérification de longueur côté serveur, et truncate à l'affichage avec minmax(0, ...).
  4. Budget d'itérations de design : prévoyez 10 à 16 itérations par composant et construisez une boucle de retour rapide dès le départ.

La question de savoir si cela fait réellement bouger le taux d'abandon reste ouverte. Nous publierons le suivi avec deux semaines de données réelles.

Des heatmaps depuis Claude Code — gratuit pour commencer.

Ajoutez une balise de tracking, recevez analyses et suggestions CRO depuis la CLI.