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 ​

Architecture thèmes × modes ​

Le système repose sur deux dimensions orthogonales :

DimensionValeursRôle
Thème (data-theme)default | ocean | forest | sunsetIdentité visuelle / palette de couleurs
Mode (data-theme-mode)light | dark | systemLuminosité / contraste

Chaque thème est disponible en mode clair et en mode sombre.

Thèmes disponibles ​

  • Default - Palette neutre classique, adapté à tout contexte
  • 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

Modes disponibles ​

  • light - Mode clair (par défaut)
  • dark - Mode sombre optimisé pour les environnements à faible luminosité
  • system - Suit automatiquement la préférence système (prefers-color-scheme)

Comportement par défaut

Sans aucun attribut HTML, le thème default en mode light est appliqué via un fallback CSS sur html:not([data-theme]). Le mode system n'est pas activé par défaut - l'utilisateur doit le choisir explicitement.

Noms dépréciés

Les noms de thème light et dark (anciens thèmes autonomes) sont dépréciés. Ils continuent de fonctionner en CSS et JS pour la rétrocompatibilité, mais émettent un avertissement console. Utilisez default + themeMode à la place.

Stratégie CSS - double attribut HTML ​

Le thème et le mode sont deux attributs HTML séparés sur <html> :

html
<!-- Thème default en mode clair -->
<html data-theme="default" data-theme-mode="light">

<!-- Thème Ocean en mode sombre -->
<html data-theme="ocean" data-theme-mode="dark">

<!-- Thème Forest qui suit la préférence système -->
<html data-theme="forest" data-theme-mode="system">

La rétrocompatibilité est assurée : [data-theme='light'] et [data-theme='dark'] fonctionnent toujours en CSS (alias définis dans default/index.scss).

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: ['default', 'ocean', 'forest', 'sunset'],
  defaultTheme: 'default',
  defaultThemeMode: 'light',
  prefix: 'su',
  highContrast: true,
  reducedMotion: true,
  storageKey: 'su-theme-config',
};

Taille du bundle : ~425 KB total (lib + styles)

Configuration minimale ​

typescript
// theme.config.ts
export const themeConfig = {
  themes: ['default'],           // Seulement le thème par défaut
  defaultTheme: 'default',
  defaultThemeMode: 'light',
  storageKey: 'my-company-theme',
};

Configuration personnalisée ​

typescript
// theme.config.ts
export const themeConfig = {
  themes: ['default', 'ocean'], // Seulement les thèmes dont vous avez besoin
  defaultTheme: 'default',
  defaultThemeMode: '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('default', 'ocean');

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

Types TypeScript ​

typescript
// Thème (identité visuelle)
type ThemeName = 'default' | 'ocean' | 'forest' | 'sunset'

// Mode (luminosité)
type ThemeMode = 'light' | 'dark' | 'system'

// Contraste et mouvement (inchangés)
type ContrastMode = 'normal' | 'high' | 'auto'
type MotionMode   = 'normal' | 'reduce' | 'auto'

// Dépréciés -  rétrocompatibilité uniquement
type DeprecatedThemeName = 'light' | 'dark'

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 '@surgeui/ds-vue';

const {
  // État réactif
  themeName,            // Ref<ThemeName> -  identité visuelle active
  themeMode,            // Ref<ThemeMode> -  'light' | 'dark' | 'system'
  contrastMode,         // Ref<ContrastMode>
  motionMode,           // Ref<MotionMode>

  // Computed
  effectiveTheme,       // ComputedRef<ThemeName> -  thème résolu (jamais 'system')
  effectiveThemeMode,   // ComputedRef<'light' | 'dark'> -  mode résolu
  isDarkMode,           // ComputedRef<boolean>

  // Actions
  setTheme,             // (theme: ThemeName) => void
  setThemeMode,         // (mode: ThemeMode) => void
  toggleMode,           // () => void -  bascule light ↔ dark
  cycleTheme,           // () => void -  cycle entre les thèmes disponibles
  clearConfig,          // () => void -  reset vers defaultTheme + defaultThemeMode
} = useTheme();
</script>

Options du composable ​

typescript
useTheme({
  // Surcharger les thèmes disponibles
  availableThemes: ['default', 'ocean'],

  // Thème par défaut
  defaultTheme: 'default',

  // Mode par défaut (NOUVEAU)
  defaultThemeMode: 'light',

  // Clé localStorage personnalisée
  storageKey: 'my-app-theme',

  // Désactiver la persistance
  persist: false
});

Changer de thème ​

Changer l'identité visuelle ​

vue
<template>
  <button @click="setTheme('ocean')">Thème Océan</button>
  <button @click="setTheme('forest')">Thème Forêt</button>
  <button @click="setTheme('default')">Thème par défaut</button>
</template>

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

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

Changer le mode de luminosité ​

vue
<template>
  <button @click="setThemeMode('dark')">Mode sombre</button>
  <button @click="setThemeMode('light')">Mode clair</button>
  <button @click="setThemeMode('system')">Suivre le système</button>
</template>

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

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

Toggle mode clair/sombre ​

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

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

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

Cycle entre thèmes ​

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

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

const { cycleTheme, effectiveTheme } = 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 mode (clair / sombre / systè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 modes.

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

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

Fonctionnalités :

  • Icône dynamique selon le mode actif
  • Label du mode (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 '@surgeui/ds-vue';

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 '@surgeui/ds-vue';

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 '@surgeui/ds-vue';

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);

  // Section inversée (fond sombre sur page claire, ou inverse)
  &--inverted {
    background-color: var(--su-bg-inverse);
    color: var(--su-text-on-inverse);
    border-color: var(--su-border-inverse);
  }

  // É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. Combiner thème et mode avec couleurs de marque ​

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

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

const applyDarkWithBrand = () => {
  // Changer vers thème default en mode sombre
  setTheme('default')
  setThemeMode('dark')

  // Surcharger avec couleurs de marque
  setCustomVariable('primaryDefault', '#db2777')
  setCustomVariable('primaryHover', '#be185d')
}
</script>

<template>
  <button @click="applyDarkWithBrand">
    Mode 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
  • --su-custom-bg-inverse
  • --su-custom-bg-inverse-subtle

Bordures :

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

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 et tous les modes ✅ Persistable - Compatible avec localStorage pour la sauvegarde utilisateur

Créer un thème personnalisé ​

1. Nouvelle structure de fichiers (light + dark séparés) ​

styles/themes/
├── _registry.scss
├── _schema.scss          ← Validation des tokens requis
├── default/
│   ├── _color.scss
│   ├── index.scss
│   └── tokens/
│       ├── light.scss
│       └── dark.scss
├── ocean/    (même structure)
├── forest/   (même structure)
├── sunset/   (même structure)
└── custom/   ← Votre thème
    ├── index.scss
    └── tokens/
        ├── light.scss
        └── dark.scss

2. Définir les tokens light ​

scss
// themes/custom/tokens/light.scss
$theme-custom-light: (
  '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. Définir les tokens dark ​

scss
// themes/custom/tokens/dark.scss
$theme-custom-dark: (
  'text-primary': #f7fafc,
  'text-secondary': #e2e8f0,
  'text-tertiary': #a0aec0,

  'bg-canvas': #1a202c,
  'bg-surface': #2d3748,

  'border-default': #4a5568,
  'border-focus': #63b3ed,

  'primary-default': #63b3ed,
  'primary-hover': #4299e1,
  'primary-text': #1a202c,

  // ... autres tokens
);

4. Enregistrer le thème avec les deux modes ​

scss
// themes/custom/index.scss
@use '../_registry' as registry;
@use './tokens/light' as *;
@use './tokens/dark' as *;

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

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

5. Ajouter les métadonnées TypeScript ​

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;
  defaultThemeMode?: ThemeMode;  // NOUVEAU
  storageKey?: string;
  persist?: boolean;
}

Retour ​

typescript
{
  // État réactif
  themeName: Ref<ThemeName>;
  themeMode: Ref<ThemeMode>;             // NOUVEAU
  contrastMode: Ref<ContrastMode>;
  motionMode: Ref<MotionMode>;

  // Computed
  effectiveTheme: ComputedRef<ThemeName>;
  effectiveThemeMode: ComputedRef<'light' | 'dark'>;  // NOUVEAU
  effectiveContrast: ComputedRef<'normal' | 'high'>;
  effectiveMotion: ComputedRef<'normal' | 'reduce'>;
  systemContrast: ComputedRef<'normal' | 'high'>;
  systemMotion: ComputedRef<'normal' | 'reduce'>;
  isDarkMode: ComputedRef<boolean>;

  // Données
  availableThemes: ComputedRef<ThemeMetadata[]>;

  // Actions
  setTheme: (theme: ThemeName) => void;
  setThemeMode: (mode: ThemeMode) => void;   // NOUVEAU
  toggleMode: () => void;                    // NOUVEAU -  remplace toggleTheme
  cycleTheme: () => void;
  setContrast: (contrast: ContrastMode) => void;
  setMotion: (motion: MotionMode) => void;
  clearConfig: () => void;
}

toggleTheme déprécié

toggleTheme() est conservé pour la rétrocompatibilité mais émet un avertissement console. Utilisez toggleMode() à la place.

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 '@surgeui/ds-vue';
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 '@surgeui/ds-vue';

const { themeName, themeMode, effectiveThemeMode } = useTheme({
  defaultTheme: 'default',
  defaultThemeMode: 'system',
  storageKey: 'user-preferences-theme',
  persist: true
});

// Tracking analytique du changement de thème
watch(effectiveThemeMode, (newMode) => {
  console.log('Mode changed to:', newMode);
  // analytics.track('theme_mode_changed', { mode: newMode });
});
</script>

Thème forcé pour une section ​

vue
<template>
  <!-- Force le thème default en mode sombre pour cette section -->
  <div data-theme="default" data-theme-mode="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>

Section inversée avec les tokens d'inversion ​

vue
<template>
  <!-- Section qui contraste avec le contexte sans forcer un thème entier -->
  <div class="hero-section">
    <h1>Titre accrocheur</h1>
    <p>Description mise en avant</p>
  </div>
</template>

<style scoped lang="scss">
.hero-section {
  background-color: var(--su-bg-inverse);
  color: var(--su-text-on-inverse);
  border: 1px solid var(--su-border-inverse);

  // La surface secondaire sur fond inversé
  .hero-section__card {
    background-color: var(--su-surface-inverse);
  }
}
</style>

Performances ​

Optimisations intégrées ​

  • CSS Variables : Changement de thème instantané sans rechargement
  • Double attribut : Sélecteurs CSS ciblés par [data-theme][data-theme-mode]
  • Lazy loading : Seuls les thèmes configurés sont inclus dans le bundle
  • Tree-shaking : Thèmes non importés = 0 bytes
  • localStorage : Persistance sans overhead réseau

Comparaison de taille ​

ConfigurationTaille bundle totalThèmes inclus
Minimal~200 KBDefault uniquement
Standard~300 KBDefault + 1 thème coloré
Complet~425 KBTous les thèmes

Recommandations ​

Applications corporate

Utilisez uniquement default 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
  4. Les attributs data-theme et data-theme-mode sont bien présents sur <html>

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).

Avertissement console sur setTheme('dark') ​

setTheme('dark') et setTheme('light') sont dépréciés. Remplacez par :

typescript
// Avant (déprécié)
setTheme('dark')

// Après
setThemeMode('dark')

Ressources ​

Publié sous licence MIT.