Skip to content

Système de Thèmes

Le Design System propose un système de thèmes complet et performant avec support du mode sombre, du contraste élevé et de la réduction des animations.

Vue d'ensemble

Thèmes disponibles

Le système propose 5 thèmes organisés en deux catégories :

Thèmes système

  • Light - Thème clair classique, idéal pour une utilisation en journée
  • Dark - Thème sombre optimisé pour les environnements à faible luminosité

Thèmes colorés

  • Ocean - Palette maritime avec bleu océan et accents corail
  • Forest - Inspiration naturelle avec verts forêt et orange automne
  • Sunset - Ambiance chaleureuse avec rose, violet et orange

Mode Auto

Le mode auto détecte automatiquement les préférences système de l'utilisateur (prefers-color-scheme) et applique le thème correspondant.

Installation et Configuration

Configuration du build

Pour optimiser la taille du bundle CSS, vous pouvez choisir quels thèmes inclure lors du build.

Configuration par défaut (recommandée)

typescript
// theme.config.ts
export const themeConfig = {
  themes: ['light', 'dark'], // Seulement les thèmes essentiels
  defaultTheme: 'auto',
  prefix: 'su',
  highContrast: true,
  reducedMotion: true,
  storageKey: 'su-theme-config',
};

Taille du bundle : ~8-10 KB CSS

Tous les thèmes

typescript
// theme.config.ts
export const themeConfig = {
  themes: ['light', 'dark', 'ocean', 'forest', 'sunset'],
  defaultTheme: 'auto',
};

Taille du bundle : ~25-30 KB CSS

Configuration personnalisée

typescript
// theme.config.ts
export const themeConfig = {
  themes: ['light', 'dark', 'ocean'], // Seulement les thèmes dont vous avez besoin
  defaultTheme: 'light',
  storageKey: 'my-company-theme',
};

Chargement dans le SCSS

scss
// main.scss
@use './core/theme-loader' as loader;

// Option 1 : Thèmes spécifiques
@include loader.load-themes('light', 'dark');

// Option 2 : Tous les thèmes
@include loader.load-all-themes();

// Option 3 : Sélection personnalisée
@include loader.load-themes('light', 'dark', 'ocean');

Utilisation

Composable useTheme

Le composable useTheme est le point d'entrée principal pour gérer les thèmes.

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { 
  themeName,           // Thème sélectionné ('auto' | 'light' | 'dark' | 'ocean' | 'forest' | 'sunset')
  effectiveTheme,      // Thème effectif appliqué
  setTheme,            // Changer de thème
  toggleTheme,         // Basculer light/dark
  availableThemes,     // Liste des thèmes disponibles
  isDarkMode           // Booléen pour savoir si on est en mode sombre
} = useTheme();
</script>

Options du composable

typescript
useTheme({
  // Surcharger les thèmes disponibles
  availableThemes: ['light', 'dark', 'ocean'],
  
  // Thème par défaut
  defaultTheme: 'auto',
  
  // Clé localStorage personnalisée
  storageKey: 'my-app-theme',
  
  // Désactiver la persistance
  persist: false
});

Changer de thème

Méthode directe

vue
<template>
  <button @click="setTheme('dark')">Mode sombre</button>
  <button @click="setTheme('ocean')">Thème Océan</button>
  <button @click="setTheme('auto')">Automatique</button>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { setTheme } = useTheme();
</script>

Toggle simple

vue
<template>
  <button @click="toggleTheme">
    {{ isDarkMode ? '☀️ Clair' : '🌙 Sombre' }}
  </button>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { toggleTheme, isDarkMode } = useTheme();
</script>

Cycle entre thèmes

vue
<template>
  <button @click="cycleTheme">
    Thème suivant : {{ currentThemeMetadata.name }}
  </button>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { cycleTheme, currentThemeMetadata } = useTheme();
</script>

Composants

ThemeSelector

Composant complet pour la sélection de thème avec interface visuelle.

vue
<template>
  <ThemeSelector />
</template>

<script setup lang="ts">
import ThemeSelector from '@/components/ThemeSelector.vue';
</script>

Fonctionnalités :

  • Prévisualisation visuelle de chaque thème
  • Sélection du contraste (normal / élevé)
  • Configuration des animations (normales / réduites)
  • Affichage des préférences système détectées
  • Bouton de réinitialisation

ThemeToggle

Bouton compact pour basculer rapidement entre les thèmes.

vue
<template>
  <ThemeToggle />
</template>

<script setup lang="ts">
import ThemeToggle from '@/components/ThemeToggle.vue';
</script>

Fonctionnalités :

  • Icône dynamique selon le thème actif
  • Label du thème (masqué sur mobile)
  • Animation de transition
  • Cycle entre tous les thèmes disponibles

Accessibilité

Contraste élevé

Le système supporte automatiquement le mode contraste élevé, essentiel pour l'accessibilité.

Détection automatique

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { effectiveContrast, systemContrast } = useTheme();
// effectiveContrast: 'normal' | 'high'
// systemContrast: préférence système détectée
</script>

Configuration manuelle

vue
<template>
  <select :value="contrastMode" @change="setContrast($event.target.value)">
    <option value="auto">Automatique</option>
    <option value="normal">Normal</option>
    <option value="high">Élevé</option>
  </select>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { contrastMode, setContrast } = useTheme();
</script>

Comportement en mode contraste élevé

  • Couleurs pures (noir pur / blanc pur)
  • Bordures renforcées
  • Suppression des effets de transparence
  • Contraste minimum de 7:1 (WCAG AAA)

Réduction des animations

Respect de prefers-reduced-motion pour les utilisateurs sensibles aux mouvements.

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';

const { motionMode, setMotion } = useTheme();
</script>

<template>
  <select :value="motionMode" @change="setMotion($event.target.value)">
    <option value="auto">Automatique</option>
    <option value="normal">Activées</option>
    <option value="reduce">Réduites</option>
  </select>
</template>

Effet :

  • Toutes les transitions deviennent instantanées (0ms)
  • Les animations sont désactivées
  • Les transformations scale sont neutralisées

Utilisation dans les composants

Accéder aux tokens de couleur

Utilisez les CSS variables générées automatiquement :

vue
<style scoped lang="scss">
.my-component {
  // Texte
  color: var(--su-text-primary);
  
  // Background
  background-color: var(--su-bg-surface);
  
  // Bordure
  border: 1px solid var(--su-border-default);
  
  // Couleur primaire
  background-color: var(--su-primary-default);
  
  // États
  &--success {
    color: var(--su-state-success);
    background-color: var(--su-state-success-bg);
  }
  
  &:hover {
    background-color: var(--su-bg-hover);
  }
  
  &:focus-visible {
    border-color: var(--su-border-focus);
  }
}
</style>

Mixins utilitaires

vue
<style scoped lang="scss">
@use '@/styles/core/mixins' as *;

.my-button {
  // Transitions automatiques avec support reduced-motion
  @include transition(background-color, transform);
  
  // Focus ring accessible
  &:focus-visible {
    @include focus-ring;
  }
  
  // États interactifs complets
  @include interactive-states;
  
  // Surface surélevée
  @include surface($elevated: true);
}
</style>

Personnalisation avancée avec variables CSS Custom

Le système de thèmes supporte la personnalisation dynamique des couleurs via des variables CSS custom. Cela vous permet de surcharger les couleurs du thème actif sans créer un thème complet.

Comment ça fonctionne

Le système de variables custom utilise un mécanisme de fallback CSS :

scss
// Généré automatiquement pour chaque token
--su-primary-default: var(--su-custom-primary-default, #3b82f6);

Flux :

  1. L'utilisateur définit --su-custom-primary-default: #dc2626
  2. CSS résout var(--su-custom-primary-default, ...)#dc2626
  3. Tous les composants utilisant --su-primary-default reçoivent la nouvelle valeur
  4. Si pas de custom défini, fallback sur la valeur du thème

Composable useCustomTheme

Le composable useCustomTheme permet de gérer les variables CSS custom de manière réactive.

vue
<script setup lang="ts">
import { useCustomTheme } from '@surgeui/ds-vue'

const { 
  applyCustomTheme,    // Applique un thème custom entier
  setCustomVariable,   // Définit une variable individuelle
  getCustomTheme,      // Récupère le thème custom actuel
  resetCustomTheme,    // Réinitialise tous les customs
  mergeWithTheme,      // Fusionne avec un thème existant
  customTheme          // État réactif du thème custom
} = useCustomTheme()
</script>

Exemples d'utilisation

1. Appliquer un thème custom complet

vue
<script setup lang="ts">
import { useCustomTheme } from '@surgeui/ds-vue'

const { applyCustomTheme } = useCustomTheme()

const applyBrandTheme = () => {
  applyCustomTheme({
    textPrimary: '#1f2937',
    bgSurface: '#ffffff',
    bgCanvas: '#f9fafb',
    primaryDefault: '#dc2626',
    primaryHover: '#b91c1c',
    primaryActive: '#991b1b',
    primaryText: '#ffffff',
    stateSuccess: '#059669',
    stateError: '#dc2626'
  })
}
</script>

<template>
  <button @click="applyBrandTheme">
    Appliquer thème marque
  </button>
</template>

2. Modifier une couleur individuellement

vue
<script setup lang="ts">
import { useCustomTheme } from '@surgeui/ds-vue'

const { setCustomVariable } = useCustomTheme()

// Simple changement de couleur
setCustomVariable('primaryDefault', '#3b82f6')

// Réactif avec input
const handleColorChange = (color: string) => {
  setCustomVariable('primaryDefault', color)
}
</script>

<template>
  <input 
    type="color" 
    @input="(e) => handleColorChange((e.target as HTMLInputElement).value)"
    placeholder="Sélectionner couleur primaire"
  >
</template>

3. Sélecteur de couleurs interactif

vue
<script setup lang="ts">
import { useCustomTheme } from '@surgeui/ds-vue'
import { ref } from 'vue'

const { setCustomVariable, resetCustomTheme } = useCustomTheme()

const colors = ref({
  primary: '#3b82f6',
  secondary: '#6b7280',
  success: '#10b981',
  error: '#ef4444'
})

const applyColor = (name: string, value: string) => {
  colors.value[name as keyof typeof colors.value] = value
  
  // Mapper aux propriétés du thème
  const propertyMap: Record<string, string> = {
    primary: 'primaryDefault',
    secondary: 'secondaryDefault',
    success: 'stateSuccess',
    error: 'stateError'
  }
  
  setCustomVariable(propertyMap[name], value)
}

const reset = () => {
  colors.value = {
    primary: '#3b82f6',
    secondary: '#6b7280',
    success: '#10b981',
    error: '#ef4444'
  }
  resetCustomTheme()
}
</script>

<template>
  <div class="color-picker">
    <div v-for="(color, name) in colors" :key="name" class="color-item">
      <label>{{ name }}</label>
      <input 
        type="color" 
        :value="color"
        @input="(e) => applyColor(name, (e.target as HTMLInputElement).value)"
      >
      <code>{{ color }}</code>
    </div>
    <button @click="reset">Réinitialiser</button>
  </div>
</template>

<style scoped lang="scss">
.color-picker {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
}

.color-item {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  
  input[type="color"] {
    width: 44px;
    height: 44px;
    border: 2px solid var(--su-border-default);
    border-radius: 6px;
    cursor: pointer;
  }
}
</style>

4. Combiner avec useTheme()

vue
<script setup lang="ts">
import { useTheme } from '@surgeui/ds-vue'
import { useCustomTheme } from '@surgeui/ds-vue'

const { setTheme } = useTheme()
const { setCustomVariable } = useCustomTheme()

const applyDarkWithBrand = () => {
  // Changer de thème
  setTheme('dark')
  
  // Surcharger avec couleurs de marque
  setCustomVariable('primaryDefault', '#db2777')
  setCustomVariable('primaryHover', '#be185d')
}
</script>

<template>
  <button @click="applyDarkWithBrand">
    Thème sombre + Marque
  </button>
</template>

Variables CSS disponibles pour personnalisation

Vous pouvez personnaliser n'importe quel token de thème en utilisant le format --su-custom-<token-name> :

Texte :

  • --su-custom-text-primary
  • --su-custom-text-secondary
  • --su-custom-text-tertiary
  • --su-custom-text-disabled
  • --su-custom-text-inverse

Backgrounds :

  • --su-custom-bg-canvas
  • --su-custom-bg-surface
  • --su-custom-bg-surface-elevated
  • --su-custom-bg-hover
  • --su-custom-bg-active
  • --su-custom-bg-selected
  • --su-custom-bg-disabled

Bordures :

  • --su-custom-border-default
  • --su-custom-border-subtle
  • --su-custom-border-strong
  • --su-custom-border-focus
  • --su-custom-border-disabled

Actions :

  • --su-custom-primary-default
  • --su-custom-primary-hover
  • --su-custom-primary-active
  • --su-custom-primary-disabled
  • --su-custom-primary-text
  • --su-custom-secondary-default
  • --su-custom-secondary-hover
  • --su-custom-secondary-active
  • --su-custom-secondary-disabled
  • --su-custom-secondary-text

États :

  • --su-custom-state-success
  • --su-custom-state-success-bg
  • --su-custom-state-warning
  • --su-custom-state-warning-bg
  • --su-custom-state-error
  • --su-custom-state-error-bg
  • --su-custom-state-info
  • --su-custom-state-info-bg

Avantages de cette approche

Dynamique - Changement instantané sans rechargement
Flexible - Personnaliser partiellement ou entièrement le thème
Performant - Pas de SCSS recompilation, uniquement du CSS
Compatible - Fonctionne avec tous les thèmes (light, dark, ocean, etc.)
Persistable - Compatible avec localStorage pour la sauvegarde utilisateur

Tokens disponibles

Texte

TokenDescription
--su-text-primaryTexte principal (contraste maximal)
--su-text-secondaryTexte secondaire
--su-text-tertiaryTexte tertiaire (moins important)
--su-text-disabledTexte désactivé
--su-text-inverseTexte sur fond sombre

Liens

TokenDescription
--su-link-defaultCouleur par défaut des liens
--su-link-hoverCouleur au survol
--su-link-visitedLiens visités
--su-link-mutedLiens secondaires

Backgrounds

TokenDescription
--su-bg-canvasFond global de l'application
--su-bg-surfaceFond des cartes, modales
--su-bg-surface-elevatedSurface surélevée (ombres)
--su-bg-hoverFond au survol
--su-bg-activeFond en état actif
--su-bg-selectedFond sélectionné
--su-bg-disabledFond désactivé

Bordures

TokenDescription
--su-border-defaultBordure par défaut
--su-border-subtleBordure subtile
--su-border-strongBordure renforcée
--su-border-focusBordure de focus (accessibilité)
--su-border-disabledBordure désactivée

États

TokenDescription
--su-state-successCouleur de succès
--su-state-success-bgFond de succès
--su-state-warningCouleur d'avertissement
--su-state-warning-bgFond d'avertissement
--su-state-errorCouleur d'erreur
--su-state-error-bgFond d'erreur
--su-state-infoCouleur d'information
--su-state-info-bgFond d'information

Actions primaires

TokenDescription
--su-primary-defaultCouleur primaire
--su-primary-hoverPrimaire au survol
--su-primary-activePrimaire en état actif
--su-primary-disabledPrimaire désactivé
--su-primary-textTexte sur primaire

Actions secondaires

TokenDescription
--su-secondary-defaultCouleur secondaire
--su-secondary-hoverSecondaire au survol
--su-secondary-activeSecondaire en état actif
--su-secondary-disabledSecondaire désactivé
--su-secondary-textTexte sur secondaire

Créer un thème personnalisé

1. Créer la structure

styles/
└── themes/
    └── custom/
        ├── _tokens.scss
        └── index.scss

2. Définir les tokens

scss
// themes/custom/_tokens.scss
@use '../../foundations/colors' as *;

$theme-custom: (
  'text-primary': #1a202c,
  'text-secondary': #2d3748,
  'text-tertiary': #4a5568,
  
  'bg-canvas': #f7fafc,
  'bg-surface': #ffffff,
  
  'border-default': #cbd5e0,
  'border-focus': #4299e1,
  
  'primary-default': #4299e1,
  'primary-hover': #3182ce,
  'primary-text': #ffffff,
  
  // ... autres tokens
);

3. Enregistrer le thème

scss
// themes/custom/index.scss
@use '../_registry' as registry;
@use './_tokens' as *;

@include registry.register-theme('custom', $theme-custom);

[data-theme='custom'] {
  @include registry.generate-theme-vars($theme-custom);
}

4. Charger le thème

scss
// main.scss
@include loader.load-themes('light', 'dark', 'custom');

5. Ajouter les métadonnées

typescript
// composables/useTheme.ts
const ALL_THEMES: ThemeMetadata[] = [
  // ... autres thèmes
  {
    id: 'custom',
    name: 'Personnalisé',
    description: 'Mon thème custom',
    category: 'color',
    preview: { 
      primary: '#4299e1', 
      background: '#f7fafc', 
      surface: '#ffffff' 
    },
    available: themeConfig.themes.includes('custom')
  }
];

API Reference

useTheme(options?)

Options

typescript
interface UseThemeOptions {
  availableThemes?: ThemeName[];
  defaultTheme?: ThemeName;
  storageKey?: string;
  persist?: boolean;
}

Retour

typescript
{
  // État réactif
  themeName: Ref<ThemeName>;
  contrastMode: Ref<ContrastMode>;
  motionMode: Ref<MotionMode>;
  
  // Computed
  effectiveTheme: ComputedRef<Exclude<ThemeName, 'auto'>>;
  effectiveContrast: ComputedRef<'normal' | 'high'>;
  effectiveMotion: ComputedRef<'normal' | 'reduce'>;
  systemTheme: ComputedRef<'light' | 'dark'>;
  systemContrast: ComputedRef<'normal' | 'high'>;
  systemMotion: ComputedRef<'normal' | 'reduce'>;
  currentThemeMetadata: ComputedRef<ThemeMetadata>;
  isDarkMode: ComputedRef<boolean>;
  
  // Données
  availableThemes: ComputedRef<ThemeMetadata[]>;
  systemThemes: ComputedRef<ThemeMetadata[]>;
  colorThemes: ComputedRef<ThemeMetadata[]>;
  
  // Actions
  setTheme: (theme: ThemeName) => void;
  setContrast: (contrast: ContrastMode) => void;
  setMotion: (motion: MotionMode) => void;
  toggleTheme: () => void;
  cycleTheme: () => void;
  clearConfig: () => void;
}

Exemples pratiques

Dashboard avec sélecteur de thème

vue
<template>
  <div class="dashboard">
    <header class="dashboard-header">
      <h1>Mon Dashboard</h1>
      <ThemeToggle />
    </header>
    
    <aside class="dashboard-sidebar">
      <ThemeSelector />
    </aside>
    
    <main class="dashboard-content">
      <!-- Contenu -->
    </main>
  </div>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme';
import ThemeToggle from '@/components/ThemeToggle.vue';
import ThemeSelector from '@/components/ThemeSelector.vue';

useTheme();
</script>

<style scoped lang="scss">
.dashboard {
  min-height: 100vh;
  background-color: var(--su-bg-canvas);
  color: var(--su-text-primary);
}

.dashboard-header {
  display: flex;
  justify-content: space-between;
  padding: var(--su-spacing-4);
  background-color: var(--su-bg-surface);
  border-bottom: 1px solid var(--su-border-default);
}
</style>

Préférence utilisateur persistante

vue
<script setup lang="ts">
import { watch } from 'vue';
import { useTheme } from '@/composables/useTheme';

const { themeName, effectiveTheme } = useTheme({
  defaultTheme: 'auto',
  storageKey: 'user-preferences-theme',
  persist: true
});

// Tracking analytique du changement de thème
watch(effectiveTheme, (newTheme) => {
  console.log('Theme changed to:', newTheme);
  // analytics.track('theme_changed', { theme: newTheme });
});
</script>

Thème forcé pour une section

vue
<template>
  <!-- Force le thème dark pour cette section -->
  <div data-theme="dark" class="promo-section">
    <h2>Section avec thème forcé</h2>
    <p>Cette section reste sombre même si l'app est en mode clair</p>
  </div>
</template>

<style scoped lang="scss">
.promo-section {
  padding: var(--su-spacing-8);
  background-color: var(--su-bg-canvas);
  color: var(--su-text-primary);
}
</style>

Performances

Optimisations intégrées

  • CSS Variables : Changement de thème instantané sans rechargement
  • Lazy loading : Seuls les thèmes configurés sont inclus dans le bundle
  • Tree-shaking : Thèmes non importés = 0 bytes
  • Media queries : Détection native des préférences système
  • localStorage : Persistance sans overhead réseau

Comparaison de taille

ConfigurationTaille CSSThèmes inclus
Minimal~8 KBLight + Dark
Standard~15 KBLight + Dark + 1 coloré
Complet~30 KBTous les thèmes

Recommandations

Applications corporate

Utilisez uniquement light et dark pour minimiser la taille du bundle.

Applications créatives

Incluez tous les thèmes pour offrir une expérience personnalisée riche.

Sites marketing

Choisissez 1-2 thèmes colorés alignés avec votre identité de marque.

Dépannage

Le thème ne s'applique pas

Vérifiez que :

  1. Le thème est bien inclus dans theme.config.ts
  2. Le thème est chargé dans main.scss
  3. useTheme() est appelé dans App.vue ou un parent

Les couleurs ne changent pas

Assurez-vous d'utiliser les CSS variables :

scss
// ❌ Mauvais
color: #111827;

// ✅ Bon
color: var(--su-text-primary);

Thème non disponible en production

Vérifiez que le thème est bien dans la liste themes de theme.config.ts et qu'il est importé dans le loader.

Préférences non sauvegardées

Vérifiez que persist: true est configuré et que localStorage est accessible (pas en navigation privée).

Ressources

Publié sous licence MIT.