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 :
| Dimension | Valeurs | Rôle |
|---|---|---|
Thème (data-theme) | default | ocean | forest | sunset | Identité visuelle / palette de couleurs |
Mode (data-theme-mode) | light | dark | system | Luminosité / 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> :
<!-- 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)
// 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
// 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
// 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
// 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
// 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.
<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
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
<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é
<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
<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
<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.
<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.
<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
<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
<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.
<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 :
<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
<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 :
// Généré automatiquement pour chaque token
--su-primary-default: var(--su-custom-primary-default, #3b82f6);Flux :
- L'utilisateur définit
--su-custom-primary-default: #dc2626 - CSS résout
var(--su-custom-primary-default, ...)→#dc2626 - Tous les composants utilisant
--su-primary-defaultreçoivent la nouvelle valeur - 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.
<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
<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
<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
<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.scss2. Définir les tokens light
// 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
// 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
// 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
// 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
interface UseThemeOptions {
availableThemes?: ThemeName[];
defaultTheme?: ThemeName;
defaultThemeMode?: ThemeMode; // NOUVEAU
storageKey?: string;
persist?: boolean;
}Retour
{
// É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
<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
<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
<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
<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
| Configuration | Taille bundle total | Thèmes inclus |
|---|---|---|
| Minimal | ~200 KB | Default uniquement |
| Standard | ~300 KB | Default + 1 thème coloré |
| Complet | ~425 KB | Tous 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 :
- Le thème est bien inclus dans
theme.config.ts - Le thème est chargé dans
main.scss useTheme()est appelé dansApp.vueou un parent- Les attributs
data-themeetdata-theme-modesont bien présents sur<html>
Les couleurs ne changent pas
Assurez-vous d'utiliser les CSS variables :
// ❌ 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 :
// Avant (déprécié)
setTheme('dark')
// Après
setThemeMode('dark')