Apogée Consult
Retour au blog
Mathieu Ponton
Mathieu PontonCo-Founder & ingénieur logiciel

Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.

Tailwind v4 et design tokens: notre système pour ne plus écrire deux fois la même couleur

  • ui-ux

Avec Tailwind v4 et sa directive @theme, on a migré notre design system vers des tokens CSS natifs. Moins de configuration JavaScript, plus de cohérence entre composants.

Tailwind v4 et design tokens: notre système pour ne plus écrire deux fois la même couleur

Sur un projet maintenu depuis 18 mois, on avait la même couleur de brand écrite à cinq endroits différents : tailwind.config.ts, deux fichiers CSS globaux, un fichier de thème shadcn/ui, et un composant qui utilisait une valeur en dur. Quand le client a changé sa couleur principale lors d'une refonte, il a fallu 40 minutes pour identifier et remplacer toutes les occurrences.

Avec Tailwind v4, on a restructuré notre approche. Ce n'est pas une migration triviale, mais le résultat est cohérent.

Ce que change Tailwind v4

Tailwind v4 abandonne le fichier tailwind.config.ts pour la configuration du thème. Tout passe par CSS, via la directive @theme dans le fichier d'entrée. C'est un changement de paradigme : la configuration devient du CSS standard, pas du JavaScript.

Avant (Tailwind v3) :

// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          DEFAULT: "#044477",
          light: "#3b82c4",
          dark: "#022d52",
        },
      },
    },
  },
}

Après (Tailwind v4) :

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

@theme {
  --color-brand: #044477;
  --color-brand-light: #3b82c4;
  --color-brand-dark: #022d52;
}

Ces variables @theme sont exposées comme classes utilitaires Tailwind (bg-brand, text-brand-light) et comme variables CSS natives (var(--color-brand)). Un token, deux usages.

Notre structure de tokens

On organise les tokens en trois couches.

Couche primitive : les valeurs brutes. On les préfixe --primitive- pour signaler qu'on ne les utilise pas directement dans les composants.

@theme {
  /* Primitives couleur */
  --color-primitive-blue-900: #022d52;
  --color-primitive-blue-700: #044477;
  --color-primitive-blue-400: #3b82c4;
  --color-primitive-gray-950: #0a0a0a;
  --color-primitive-gray-50: #f9fafb;

  /* Primitives typographie */
  --font-size-primitive-xs: 0.75rem;
  --font-size-primitive-sm: 0.875rem;
  --font-size-primitive-base: 1rem;
  --font-size-primitive-lg: 1.125rem;
  --font-size-primitive-xl: 1.25rem;
  --font-size-primitive-2xl: 1.5rem;
}

Couche sémantique : les tokens avec une signification métier. On les référence depuis les primitives.

@layer base {
  :root {
    --color-background: var(--color-primitive-gray-50);
    --color-foreground: var(--color-primitive-gray-950);
    --color-primary: var(--color-primitive-blue-700);
    --color-primary-hover: var(--color-primitive-blue-900);
    --color-muted: #6b7280;
    --color-border: #e5e7eb;
    --color-surface: #ffffff;
  }

  .dark {
    --color-background: var(--color-primitive-gray-950);
    --color-foreground: var(--color-primitive-gray-50);
    --color-primary: var(--color-primitive-blue-400);
    --color-primary-hover: var(--color-primitive-blue-700);
    --color-muted: #9ca3af;
    --color-border: #1f2937;
    --color-surface: #111827;
  }
}

Couche composant : les tokens spécifiques à un composant, définis dans le composant lui-même quand ils n'ont pas de valeur sémantique globale.

Exposition dans @theme

On expose les tokens sémantiques dans @theme pour qu'ils génèrent des classes Tailwind :

@theme {
  --color-background: initial;
  --color-foreground: initial;
  --color-primary: initial;
  --color-primary-hover: initial;
  --color-muted: initial;
  --color-border: initial;
  --color-surface: initial;
}

La valeur initial indique à Tailwind v4 d'enregistrer ces tokens sans leur affecter une valeur fixe dans @theme, les valeurs viennent des CSS variables définies dans @layer base. Cela permet au dark mode de fonctionner correctement.

Dans les composants, on utilise :

<button className="bg-primary text-background hover:bg-primary-hover px-4 py-2 rounded-md">
  Action
</button>

bg-primary génère background-color: var(--color-primary), qui est résolue à runtime selon la classe dark présente ou non sur <html>.

Le problème qu'on a rencontré avec shadcn/ui

shadcn/ui utilise ses propres variables CSS (--primary, --background, --muted, etc.) que Tailwind v3 expose via hsl(var(--primary)). En migrant vers Tailwind v4, on a eu un conflit : nos tokens sémantiques et les tokens shadcn portaient les mêmes noms.

La solution : on a préfixé nos tokens (--color-primary au lieu de --primary) et on a conservé les variables shadcn sans préfixe pour la compatibilité des composants shadcn existants.

/* Variables shadcn, conservées telles quelles */
:root {
  --primary: 217 91% 60%; /* format HSL attendu par shadcn */
  --background: 0 0% 100%;
}

/* Nos tokens sémantiques, préfixés */
:root {
  --color-primary: #044477;
  --color-background: #ffffff;
}

C'est un compromis pragmatique. Sur un nouveau projet sans shadcn, on n'aurait pas ce doublon.

Avantages concrets après 6 mois

La refonte client dont on parlait en introduction : quand le client a changé sa couleur principale une seconde fois, on a modifié deux lignes dans les primitives. Tous les composants ont été mis à jour automatiquement.

Les revues de code sont plus lisibles. bg-primary communique l'intention ; bg-[#044477] ne communique rien sauf une valeur magique.

Le design system est documentable. On peut générer un catalogue de tokens directement depuis les variables CSS, sans parser du TypeScript.

Ce qui reste imparfait

Les tokens de typographie sont moins bien intégrés. Tailwind v4 expose les font-sizes via @theme, mais les line-heights, letter-spacings, et font-weights nécessitent une organisation manuelle similaire. On ne l'a pas encore fait de façon aussi systématique que pour les couleurs.

La question ouverte : est-ce qu'on devrait adopter le format W3C Design Tokens (DTCG) pour les primitives, afin de pouvoir exporter les tokens vers Figma ou d'autres outils ? Notre setup actuel est optimisé pour le code mais n'est pas interopérable avec les outils de design.

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Design tokens Tailwind v4: système @theme et CSS variables | Apogée Consult