Système de Design Tokens en pratique : 5 patterns pour une architecture de thème et multi-marque évolutive

前端工程

Design Tokens : L'Infrastructure Atomique des Design Systems

Le plus grand point de douleur dans les design systems n'est pas le manque de composants—c'est le changement de thème par des surcharges, les extensions de marque par duplication et le mode sombre par des rustines. Les Design Tokens abstraient les décisions de design comme les couleurs, l'espacement et la typographie en paires clé-valeur indépendantes de la plateforme, permettant de « définir une fois, consommer partout ». En 2026, la spécification W3C Design Token a atteint le statut de Candidate Recommendation, Style Dictionary 4.0+ prend en charge la sortie multi-plateforme, et les Design Tokens sont devenus l'infrastructure standard des design systems.

Cet article parcourt 5 patterns fondamentaux, couvrant le pipeline complet de la superposition de Tokens → changement de thème → multi-marque → pipeline de build → dynamiques d'exécution.


Concepts Fondamentaux

Concept Description
Design Token Représentation atomique d'une décision de design sous forme de paire clé-valeur
Token Global Valeurs globales brutes, ex., blue-500: #3b82f6
Token Alias Alias sémantique, ex., primary: {blue-500}
Token de Composant Token au niveau du composant, ex., button-primary-bg: {primary}
Thème Une collection de valeurs de tokens, ex., thème clair/sombre
Multi-Marque Un même design system prenant en charge plusieurs ensembles de tokens de marque
Style Dictionary Outil de compilation de tokens avec sortie multi-plateforme
W3C DTCG Spécification du Design Token Community Group

Analyse du Problème : 5 Défis dans les Systèmes de Design Tokens

  1. Chaos dans la superposition des tokens : Limites floues entre les couches globales/sémantiques/composant, chaînes de référence excessivement longues
  2. Performance du changement de thème : Le changement de thème à l'exécution provoque un scintillement de repeinte
  3. Extension multi-marque : Grandes différences de tokens entre les marques, héritage complexe
  4. Sortie de build : Formats de sortie variables selon les plateformes (CSS/JS/Swift/Kotlin)
  5. Collaboration design : Les designers utilisent Figma, les développeurs utilisent du code—la synchronisation des tokens est difficile

Étape par Étape : 5 Patterns de Design Tokens

Pattern 1 : Architecture de Tokens à Trois Couches

// tokens/global/color.json
{
  "color": {
    "blue": {
      "100": { "value": "#dbeafe", "type": "color" },
      "500": { "value": "#3b82f6", "type": "color" },
      "900": { "value": "#1e3a8a", "type": "color" }
    },
    "gray": {
      "50": { "value": "#f9fafb", "type": "color" },
      "900": { "value": "#111827", "type": "color" }
    },
    "red": {
      "500": { "value": "#ef4444", "type": "color" }
    },
    "green": {
      "500": { "value": "#22c55e", "type": "color" }
    }
  }
}
// tokens/semantic/color.json
{
  "color": {
    "primary": { "value": "{color.blue.500}", "type": "color" },
    "primary-hover": { "value": "{color.blue.600}", "type": "color" },
    "background": { "value": "{color.gray.50}", "type": "color" },
    "surface": { "value": "#ffffff", "type": "color" },
    "text": { "value": "{color.gray.900}", "type": "color" },
    "text-secondary": { "value": "{color.gray.500}", "type": "color" },
    "error": { "value": "{color.red.500}", "type": "color" },
    "success": { "value": "{color.green.500}", "type": "color" },
    "border": { "value": "{color.gray.200}", "type": "color" }
  }
}
// tokens/component/button.json
{
  "button": {
    "primary": {
      "bg": { "value": "{color.primary}", "type": "color" },
      "bg-hover": { "value": "{color.primary-hover}", "type": "color" },
      "text": { "value": "#ffffff", "type": "color" },
      "border-radius": { "value": "{radius.md}", "type": "borderRadius" },
      "padding-x": { "value": "{spacing.4}", "type": "spacing" },
      "padding-y": { "value": "{spacing.2}", "type": "spacing" }
    },
    "secondary": {
      "bg": { "value": "{color.surface}", "type": "color" },
      "text": { "value": "{color.primary}", "type": "color" },
      "border": { "value": "{color.border}", "type": "color" }
    }
  }
}

Pattern 2 : Changement de Thème (Clair/Sombre)

// tokens/semantic/color.dark.json
{
  "color": {
    "primary": { "value": "{color.blue.400}", "type": "color" },
    "background": { "value": "{color.gray.900}", "type": "color" },
    "surface": { "value": "#1f2937", "type": "color" },
    "text": { "value": "{color.gray.50}", "type": "color" },
    "text-secondary": { "value": "{color.gray.400}", "type": "color" },
    "border": { "value": "{color.gray.700}", "type": "color" }
  }
}
/* Sortie du build : tokens.css */
:root {
  --color-primary: #3b82f6;
  --color-background: #f9fafb;
  --color-surface: #ffffff;
  --color-text: #111827;
  --color-border: #e5e7eb;
  --button-primary-bg: var(--color-primary);
  --button-primary-text: #ffffff;
}

[data-theme="dark"] {
  --color-primary: #60a5fa;
  --color-background: #111827;
  --color-surface: #1f2937;
  --color-text: #f9fafb;
  --color-border: #374151;
  --button-primary-bg: var(--color-primary);
}
// Changeur de thème
type Theme = 'light' | 'dark' | 'system';

class ThemeManager {
  private mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  private listeners: Set<(theme: Theme) => void> = new Set();

  init() {
    const saved = localStorage.getItem('theme') as Theme | null;
    if (saved && saved !== 'system') {
      this.apply(saved);
    } else {
      this.apply(this.mediaQuery.matches ? 'dark' : 'light');
    }
    this.mediaQuery.addEventListener('change', (e) => {
      if (this.getStored() === 'system') {
        this.apply(e.matches ? 'dark' : 'light');
      }
    });
  }

  setTheme(theme: Theme) {
    localStorage.setItem('theme', theme);
    if (theme === 'system') {
      this.apply(this.mediaQuery.matches ? 'dark' : 'light');
    } else {
      this.apply(theme);
    }
    this.listeners.forEach((fn) => fn(theme));
  }

  private apply(mode: 'light' | 'dark') {
    document.documentElement.setAttribute('data-theme', mode);
  }

  private getStored(): Theme {
    return (localStorage.getItem('theme') as Theme) || 'system';
  }

  onChange(fn: (theme: Theme) => void) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }
}

Pattern 3 : Héritage de Tokens Multi-Marque

// brands/brand-a/tokens.json
{
  "color": {
    "primary": { "value": "#3b82f6", "type": "color", "$extensions": { "brand": "A" } },
    "primary-hover": { "value": "#2563eb", "type": "color" }
  },
  "typography": {
    "font-family": { "value": "Inter, sans-serif", "type": "fontFamily" },
    "heading-weight": { "value": "700", "type": "fontWeight" }
  },
  "radius": {
    "base": { "value": "8px", "type": "borderRadius" }
  }
}
// brands/brand-b/tokens.json
{
  "color": {
    "primary": { "value": "#8b5cf6", "type": "color", "$extensions": { "brand": "B" } },
    "primary-hover": { "value": "#7c3aed", "type": "color" }
  },
  "typography": {
    "font-family": { "value": "Poppins, sans-serif", "type": "fontFamily" },
    "heading-weight": { "value": "600", "type": "fontWeight" }
  },
  "radius": {
    "base": { "value": "4px", "type": "borderRadius" }
  }
}
// Changeur de marque
type Brand = 'brand-a' | 'brand-b';

async function switchBrand(brand: Brand) {
  const brandCSS = await fetch(`/tokens/${brand}.css`).then((r) => r.text());
  let style = document.getElementById('brand-tokens') as HTMLStyleElement;
  if (!style) {
    style = document.createElement('style');
    style.id = 'brand-tokens';
    document.head.appendChild(style);
  }
  style.textContent = brandCSS;
  document.documentElement.setAttribute('data-brand', brand);
  localStorage.setItem('brand', brand);
}

Pattern 4 : Pipeline de Build Style Dictionary

// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary';

StyleDictionary.registerTransform({
  name: 'size/pxToRem',
  type: 'value',
  matcher: (token) => token.type === 'spacing' || token.type === 'fontSize',
  transformer: (token) => {
    const base = 16;
    const val = parseFloat(token.value);
    return `${val / base}rem`;
  },
});

StyleDictionary.registerTransformGroup({
  name: 'custom/css',
  transforms: [
    'attribute/cti',
    'name/kebab',
    'time/seconds',
    'size/pxToRem',
    'color/css',
  ],
});

const config = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'custom/css',
      buildPath: 'dist/css/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
          options: { outputReferences: true },
        },
      ],
    },
    js: {
      transformGroup: 'js',
      buildPath: 'dist/js/',
      files: [
        {
          destination: 'tokens.js',
          format: 'javascript/es6',
        },
        {
          destination: 'tokens.d.ts',
          format: 'typescript/es6-declarations',
        },
      ],
    },
    json: {
      transformGroup: 'json',
      buildPath: 'dist/json/',
      files: [
        {
          destination: 'tokens.json',
          format: 'json',
        },
      ],
    },
    ios: {
      transformGroup: 'ios-swift-separate',
      buildPath: 'dist/ios/',
      files: [
        {
          destination: 'DesignTokens.swift',
          format: 'ios-swift/class.swift',
          className: 'DesignTokens',
        },
      ],
    },
    android: {
      transformGroup: 'android',
      buildPath: 'dist/android/',
      files: [
        {
          destination: 'design_tokens.xml',
          format: 'android/resources',
        },
      ],
    },
  },
};

export default config;
# Builder toutes les plateformes
npx style-dictionary build

# Surveiller les changements de tokens
npx style-dictionary build --watch

Pattern 5 : Tokens Dynamiques à l'Exécution

// runtime-token-engine.ts
interface TokenValue {
  value: string;
  type: string;
  description?: string;
}

class RuntimeTokenEngine {
  private tokens: Map<string, TokenValue> = new Map();
  private subscribers: Map<string, Set<(value: string) => void>> = new Map();

  loadFromCSS() {
    const styles = getComputedStyle(document.documentElement);
    const allVars = Array.from(document.styleSheets)
      .flatMap((sheet) => {
        try {
          return Array.from(sheet.cssRules);
        } catch {
          return [];
        }
      })
      .filter((rule) => rule instanceof CSSStyleRule)
      .flatMap((rule: CSSStyleRule) => {
        const props: string[] = [];
        for (let i = 0; i < rule.style.length; i++) {
          const prop = rule.style[i];
          if (prop.startsWith('--')) {
            props.push(prop);
          }
        }
        return props;
      });

    const uniqueVars = [...new Set(allVars)];
    uniqueVars.forEach((v) => {
      const val = styles.getPropertyValue(v).trim();
      if (val) {
        this.tokens.set(v, { value: val, type: 'unknown' });
      }
    });
  }

  setToken(name: string, value: string) {
    const varName = name.startsWith('--') ? name : `--${name}`;
    document.documentElement.style.setProperty(varName, value);
    this.tokens.set(varName, { value, type: 'unknown' });
    this.notify(varName, value);
  }

  getToken(name: string): string | undefined {
    const varName = name.startsWith('--') ? name : `--${name}`;
    return getComputedStyle(document.documentElement)
      .getPropertyValue(varName)
      .trim();
  }

  subscribe(name: string, callback: (value: string) => void) {
    const varName = name.startsWith('--') ? name : `--${name}`;
    if (!this.subscribers.has(varName)) {
      this.subscribers.set(varName, new Set());
    }
    this.subscribers.get(varName)!.add(callback);
    return () => this.subscribers.get(varName)?.delete(callback);
  }

  private notify(name: string, value: string) {
    this.subscribers.get(name)?.forEach((fn) => fn(value));
  }
}

Guide des Pièges

Piège 1 : Chaînes de Référence de Tokens Trop Profondes

// ❌ Faux : chaîne de référence à 5 couches, difficile à déboguer
{
  "btn-bg": { "value": "{button.primary.bg}" },
  "button.primary.bg": { "value": "{color.primary}" },
  "color.primary": { "value": "{blue.500}" }
}

// ✅ Correct : Garder dans les 3 couches
{
  "button-primary-bg": { "value": "{color.primary}" },
  "color.primary": { "value": "#3b82f6" }
}

Piège 2 : Valeurs de Couleur Codées en Dur

/* ❌ Faux : Couleurs codées en dur */
.card { background: #ffffff; border: 1px solid #e5e7eb; }

/* ✅ Correct : Utiliser des variables de tokens */
.card { background: var(--color-surface); border: 1px solid var(--color-border); }

Piège 3 : Scintillement au Changement de Thème

<!-- ❌ Faux : Thème défini après le chargement JS, causant un scintillement -->

<!-- ✅ Correct : Script inline bloquant le rendu -->
<script>
  (function() {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark' || (!theme && matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>

Piège 4 : Surutilisation des Tokens de Composant

// ❌ Faux : Créer un token pour chaque propriété du composant
{
  "card-padding-top": { "value": "{spacing.4}" },
  "card-padding-right": { "value": "{spacing.4}" },
  "card-padding-bottom": { "value": "{spacing.4}" },
  "card-padding-left": { "value": "{spacing.4}" }
}

// ✅ Correct : Réutiliser les tokens sémantiques
{
  "card-padding": { "value": "{spacing.4}" }
}

Piège 5 : Ignorer la Validation des Types de Tokens

// ❌ Faux : Pas de validation de type, un token de couleur reçoit une valeur d'espacement
// ✅ Correct : Valider au moment du build
StyleDictionary.registerFilter({
  name: 'isValidColor',
  matcher: (token) => {
    if (token.type === 'color') {
      return /^#([0-9a-f]{3}){1,2}$/i.test(token.value) ||
             token.value.startsWith('{');
    }
    return true;
  },
});

Dépannage des Erreurs

# Message d'Erreur Cause Solution
1 Reference not found: {color.xxx} Le token référence une clé inexistante Vérifier l'orthographe du chemin de référence
2 Circular reference detected Référence circulaire de tokens Rompre la chaîne de référence circulaire
3 Invalid token value Format de valeur de token invalide Valider le format couleur/espacement
4 CSS variable not applied Spécificité du sélecteur écrasée Augmenter la spécificité du sélecteur de variable CSS
5 Theme flash on load Initialisation du thème après le rendu Script inline bloquant pour définir le thème
6 Style Dictionary build failed Erreur de format du fichier de tokens Vérifier la syntaxe JSON et $extensions
7 Token not found in output Token non correspondu par la source Vérifier le chemin glob de source
8 Platform output mismatch Règles de transformation incohérentes Unifier la configuration transformGroup
9 Figma sync conflict Tokens Figma désynchronisés avec le code Utiliser Token Studio pour la synchronisation bidirectionnelle
10 Runtime token override fails Surcharge à l'exécution sans effet Vérifier la priorité et la portée de la variable CSS

Optimisation Avancée

  1. Intégration Token Studio : Plugin Figma pour la synchronisation bidirectionnelle des tokens design-code
  2. Versionnage des Tokens : Versionnage sémantique + génération automatique de changelog
  3. Tokens pour Tests A/B : Changement dynamique de tokens à l'exécution pour des tests A/B visuels
  4. Token Lint : Plugin ESLint pour détecter les valeurs de design codées en dur dans le code
  5. Site de Documentation des Tokens : Documentation visuelle des tokens générée automatiquement

Comparaison

Dimension Variables CSS Design Tokens Configuration Tailwind Thème CSS-in-JS
Support de Plateforme Web uniquement Web/iOS/Android Web uniquement Web uniquement
Sécurité de Type
Changement de Thème
Multi-Marque Manuel Support natif Changement de config Changement de config
Collaboration Design ✅ Sync Figma
Sortie de Build CSS CSS/JS/Swift/Kotlin CSS/JS JS

Résumé : Les systèmes de Design Tokens sont l'infrastructure clé pour faire évoluer les design systems de « bibliothèques de composants » à « langages de design ». L'architecture de tokens à trois couches (global → sémantique → composant) combinée à la sortie multi-plateforme de Style Dictionary établit une source unique de vérité pour les décisions de design. En 2026, la spécification W3C DTCG en maturation stimulera la standardisation des tokens, faisant des tokens multi-marque et dynamiques à l'exécution des fonctionnalités standard pour les design systems d'entreprise.


Outils en Ligne Recommandés

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#Design Token#设计系统#主题切换#CSS变量#2026#前端工程