Table of Contents
Implementation d’un Mode Sombre avec Vue.js et la Composition API
Le mode sombre est devenu une fonctionnalite incontournable des applications web modernes. Dans ce guide complet, nous allons explorer comment implementer un systeme de themes robuste avec Vue.js 3 et la Composition API, en utilisant les CSS variables, localStorage et la detection automatique des preferences systeme.
Pourquoi le Mode Sombre est Essentiel
Accessibilite et Confort Visuel
Le mode sombre n’est pas qu’une simple preference esthetique. Il repond a des besoins reels des utilisateurs :
- Reduction de la fatigue oculaire : Les ecrans lumineux dans des environnements sombres peuvent causer une fatigue importante
- Meilleure lisibilite : Pour certains utilisateurs, notamment ceux souffrant de photophobie ou de migraines, le mode sombre est une necessite
- Utilisation nocturne : Une interface sombre perturbe moins le cycle circadien lors d’une utilisation tardive
Economie d’Energie
Sur les ecrans OLED et AMOLED, le mode sombre peut reduire significativement la consommation d’energie :
// Les ecrans OLED eteignent physiquement les pixels noirs
// Couleurs recommandees pour economiser l'energie
const darkThemeColors = {
background: '#000000', // Noir pur = pixels eteints sur OLED
surface: '#121212', // Surface legere
text: '#E1E1E1', // Texte visible sans etre eblouissant
textSecondary: '#A1A1A1', // Texte secondaire
}
Preferences Utilisateur
Les statistiques montrent que 70% a 80% des utilisateurs preferent utiliser le mode sombre lorsqu’il est disponible, particulierement sur mobile.
Le Composable useDarkMode()
Version de Base
Commencons par creer un composable simple pour gerer le mode sombre :
// src/composables/useDarkMode.ts
import { ref, watch } from 'vue'
const isDarkMode = ref(false)
export function useDarkMode() {
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value
}
const setDarkMode = (value: boolean) => {
isDarkMode.value = value
}
return {
isDarkMode,
toggleDarkMode,
setDarkMode,
}
}
Cette version basique fonctionne, mais elle a plusieurs limitations :
- Le theme n’est pas persiste entre les sessions
- Les preferences systeme ne sont pas detectees
- Le changement de theme n’est pas applique au DOM
Version Amelioree avec Persistance
Voici une version plus complete qui gere la persistance et l’application au DOM :
// src/composables/useDarkMode.ts
import { ref, watch, onMounted, computed } from 'vue'
// Etat global partage entre tous les composants
const isDarkMode = ref<boolean>(false)
const isInitialized = ref(false)
// Cle pour localStorage
const STORAGE_KEY = 'dark-mode-preference'
export function useDarkMode() {
// Appliquer la classe au document
const applyTheme = (dark: boolean) => {
if (typeof document !== 'undefined') {
if (dark) {
document.documentElement.classList.add('dark')
document.documentElement.setAttribute('data-theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
document.documentElement.setAttribute('data-theme', 'light')
}
}
}
// Sauvegarder dans localStorage
const savePreference = (dark: boolean) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(STORAGE_KEY, JSON.stringify(dark))
}
}
// Charger depuis localStorage
const loadPreference = (): boolean | null => {
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored !== null) {
return JSON.parse(stored)
}
}
return null
}
// Initialisation
const initialize = () => {
if (isInitialized.value) return
const stored = loadPreference()
if (stored !== null) {
// Utiliser la preference sauvegardee
isDarkMode.value = stored
} else {
// Detecter la preference systeme
if (typeof window !== 'undefined' && window.matchMedia) {
isDarkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches
}
}
applyTheme(isDarkMode.value)
isInitialized.value = true
}
// Toggle le mode sombre
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value
}
// Definir explicitement
const setDarkMode = (value: boolean) => {
isDarkMode.value = value
}
// Observer les changements
watch(isDarkMode, (newValue) => {
applyTheme(newValue)
savePreference(newValue)
})
// Initialiser au montage
onMounted(() => {
initialize()
})
return {
isDarkMode,
toggleDarkMode,
setDarkMode,
initialize,
}
}
Les CSS Variables pour les Themes
Definition des Variables
Les CSS variables (custom properties) sont la fondation d’un systeme de themes flexible :
/* src/styles/themes.css */
:root {
/* Theme clair (par defaut) */
--color-background: #ffffff;
--color-background-secondary: #f5f5f5;
--color-surface: #ffffff;
--color-surface-elevated: #ffffff;
--color-text-primary: #1a1a1a;
--color-text-secondary: #666666;
--color-text-muted: #999999;
--color-border: #e0e0e0;
--color-border-light: #f0f0f0;
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-light: #eff6ff;
--color-success: #22c55e;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-shadow: rgba(0, 0, 0, 0.1);
--color-overlay: rgba(0, 0, 0, 0.5);
/* Typographie */
--font-family-sans: 'Inter', system-ui, sans-serif;
--font-family-mono: 'Fira Code', monospace;
/* Espacements */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Rayons de bordure */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-full: 9999px;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 300ms ease;
--transition-slow: 500ms ease;
}
/* Theme sombre */
.dark,
[data-theme="dark"] {
--color-background: #0f0f0f;
--color-background-secondary: #1a1a1a;
--color-surface: #1f1f1f;
--color-surface-elevated: #2a2a2a;
--color-text-primary: #f5f5f5;
--color-text-secondary: #a0a0a0;
--color-text-muted: #666666;
--color-border: #333333;
--color-border-light: #2a2a2a;
--color-primary: #60a5fa;
--color-primary-hover: #3b82f6;
--color-primary-light: #1e3a5f;
--color-shadow: rgba(0, 0, 0, 0.3);
--color-overlay: rgba(0, 0, 0, 0.7);
}
Tableau des CSS Variables Recommandees
| Variable | Theme Clair | Theme Sombre | Usage |
|---|---|---|---|
--color-background | #ffffff | #0f0f0f | Fond principal |
--color-surface | #ffffff | #1f1f1f | Cartes, modales |
--color-text-primary | #1a1a1a | #f5f5f5 | Texte principal |
--color-text-secondary | #666666 | #a0a0a0 | Texte secondaire |
--color-border | #e0e0e0 | #333333 | Bordures |
--color-primary | #3b82f6 | #60a5fa | Couleur d’accent |
--color-shadow | rgba(0,0,0,0.1) | rgba(0,0,0,0.3) | Ombres |
Utilisation dans les Composants
<template>
<div class="card">
<h2 class="card-title">{{ title }}</h2>
<p class="card-content">{{ content }}</p>
</div>
</template>
<style scoped>
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
box-shadow: 0 2px 8px var(--color-shadow);
transition: background-color var(--transition-normal),
border-color var(--transition-normal);
}
.card-title {
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.card-content {
color: var(--color-text-secondary);
}
</style>
Persistance avec localStorage
Gestion Robuste du Stockage
Il est important de gerer les erreurs potentielles lors de l’acces a localStorage :
// src/utils/storage.ts
export interface StorageOptions {
key: string
defaultValue?: unknown
serializer?: {
read: (value: string) => unknown
write: (value: unknown) => string
}
}
const defaultSerializer = {
read: (value: string) => JSON.parse(value),
write: (value: unknown) => JSON.stringify(value),
}
export function useLocalStorage<T>(options: StorageOptions) {
const { key, defaultValue, serializer = defaultSerializer } = options
const isAvailable = (): boolean => {
try {
const testKey = '__storage_test__'
localStorage.setItem(testKey, testKey)
localStorage.removeItem(testKey)
return true
} catch {
return false
}
}
const get = (): T | undefined => {
if (!isAvailable()) return defaultValue as T
try {
const item = localStorage.getItem(key)
if (item === null) return defaultValue as T
return serializer.read(item) as T
} catch (error) {
console.warn(`Erreur lecture localStorage [${key}]:`, error)
return defaultValue as T
}
}
const set = (value: T): boolean => {
if (!isAvailable()) return false
try {
const serialized = serializer.write(value)
localStorage.setItem(key, serialized)
return true
} catch (error) {
console.warn(`Erreur ecriture localStorage [${key}]:`, error)
return false
}
}
const remove = (): boolean => {
if (!isAvailable()) return false
try {
localStorage.removeItem(key)
return true
} catch (error) {
console.warn(`Erreur suppression localStorage [${key}]:`, error)
return false
}
}
return {
get,
set,
remove,
isAvailable,
}
}
Integration avec le Composable
// src/composables/useDarkMode.ts (version amelioree)
import { ref, watch, onMounted } from 'vue'
import { useLocalStorage } from '@/utils/storage'
const isDarkMode = ref(false)
const isInitialized = ref(false)
const storage = useLocalStorage<boolean>({
key: 'user-theme-preference',
defaultValue: false,
})
export function useDarkMode() {
const applyTheme = (dark: boolean) => {
if (typeof document === 'undefined') return
document.documentElement.classList.toggle('dark', dark)
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light')
// Mettre a jour la meta theme-color pour le navigateur
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', dark ? '#0f0f0f' : '#ffffff')
}
}
const initialize = () => {
if (isInitialized.value) return
// Priorite : localStorage > preference systeme
const storedValue = storage.get()
if (storedValue !== undefined) {
isDarkMode.value = storedValue
} else {
// Detecter la preference systeme
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
isDarkMode.value = prefersDark.matches
}
applyTheme(isDarkMode.value)
isInitialized.value = true
}
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value
}
watch(isDarkMode, (newValue) => {
applyTheme(newValue)
storage.set(newValue)
})
onMounted(initialize)
return {
isDarkMode,
toggleDarkMode,
initialize,
}
}
Detection du Theme Systeme (prefers-color-scheme)
Ecouter les Changements en Temps Reel
Les utilisateurs peuvent changer leur preference systeme pendant qu’ils utilisent votre application :
// src/composables/useSystemTheme.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useSystemTheme() {
const prefersDark = ref(false)
let mediaQuery: MediaQueryList | null = null
const updatePreference = (event: MediaQueryListEvent | MediaQueryList) => {
prefersDark.value = event.matches
}
onMounted(() => {
if (typeof window === 'undefined') return
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
prefersDark.value = mediaQuery.matches
// Ecouter les changements
mediaQuery.addEventListener('change', updatePreference)
})
onUnmounted(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', updatePreference)
}
})
return {
prefersDark,
}
}
Integration dans le Composable Principal
// src/composables/useDarkMode.ts (version complete)
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
type ThemeMode = 'light' | 'dark' | 'system'
const currentMode = ref<ThemeMode>('system')
const isDarkMode = ref(false)
const isInitialized = ref(false)
const STORAGE_KEY = 'theme-mode'
export function useDarkMode() {
let mediaQuery: MediaQueryList | null = null
const systemPrefersDark = ref(false)
// Calculer si le theme actif est sombre
const computedIsDark = computed(() => {
if (currentMode.value === 'system') {
return systemPrefersDark.value
}
return currentMode.value === 'dark'
})
const applyTheme = () => {
const dark = computedIsDark.value
isDarkMode.value = dark
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', dark)
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light')
}
}
const handleSystemChange = (event: MediaQueryListEvent) => {
systemPrefersDark.value = event.matches
if (currentMode.value === 'system') {
applyTheme()
}
}
const setMode = (mode: ThemeMode) => {
currentMode.value = mode
if (typeof localStorage !== 'undefined') {
localStorage.setItem(STORAGE_KEY, mode)
}
applyTheme()
}
const toggleDarkMode = () => {
if (currentMode.value === 'system') {
setMode(systemPrefersDark.value ? 'light' : 'dark')
} else {
setMode(currentMode.value === 'dark' ? 'light' : 'dark')
}
}
const cycleMode = () => {
const modes: ThemeMode[] = ['light', 'dark', 'system']
const currentIndex = modes.indexOf(currentMode.value)
const nextIndex = (currentIndex + 1) % modes.length
setMode(modes[nextIndex])
}
const initialize = () => {
if (isInitialized.value) return
// Detecter la preference systeme
if (typeof window !== 'undefined' && window.matchMedia) {
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
systemPrefersDark.value = mediaQuery.matches
mediaQuery.addEventListener('change', handleSystemChange)
}
// Charger la preference sauvegardee
if (typeof localStorage !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY) as ThemeMode | null
if (stored && ['light', 'dark', 'system'].includes(stored)) {
currentMode.value = stored
}
}
applyTheme()
isInitialized.value = true
}
onMounted(initialize)
onUnmounted(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', handleSystemChange)
}
})
return {
isDarkMode,
currentMode,
toggleDarkMode,
setMode,
cycleMode,
initialize,
}
}
Transitions CSS entre les Themes
Transitions Fluides
Pour une experience utilisateur agreable, ajoutez des transitions lors du changement de theme :
/* src/styles/transitions.css */
/* Transition globale pour le changement de theme */
html.theme-transition,
html.theme-transition *,
html.theme-transition *::before,
html.theme-transition *::after {
transition:
background-color 300ms ease,
border-color 300ms ease,
color 200ms ease,
fill 200ms ease,
stroke 200ms ease,
box-shadow 300ms ease !important;
transition-delay: 0ms !important;
}
/* Animation du bouton de toggle */
.theme-toggle-button {
position: relative;
width: 3rem;
height: 3rem;
border-radius: var(--radius-full);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
cursor: pointer;
overflow: hidden;
transition: transform 150ms ease;
}
.theme-toggle-button:hover {
transform: scale(1.05);
}
.theme-toggle-button:active {
transform: scale(0.95);
}
/* Icones soleil et lune */
.theme-toggle-icon {
position: absolute;
top: 50%;
left: 50%;
width: 1.5rem;
height: 1.5rem;
transition:
transform 500ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 300ms ease;
}
.sun-icon {
transform: translate(-50%, -50%) rotate(0deg) scale(1);
opacity: 1;
}
.moon-icon {
transform: translate(-50%, -50%) rotate(-90deg) scale(0);
opacity: 0;
}
.dark .sun-icon {
transform: translate(-50%, -50%) rotate(90deg) scale(0);
opacity: 0;
}
.dark .moon-icon {
transform: translate(-50%, -50%) rotate(0deg) scale(1);
opacity: 1;
}
Composant ThemeToggle
<!-- src/components/ThemeToggle.vue -->
<template>
<button
@click="handleClick"
class="theme-toggle"
:aria-label="ariaLabel"
:title="buttonTitle"
>
<span class="theme-toggle-track">
<!-- Icone Soleil -->
<svg
class="theme-icon sun"
:class="{ active: !isDarkMode }"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
<!-- Icone Lune -->
<svg
class="theme-icon moon"
:class="{ active: isDarkMode }"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useDarkMode } from '@/composables/useDarkMode'
const { isDarkMode, toggleDarkMode } = useDarkMode()
const ariaLabel = computed(() =>
isDarkMode.value
? 'Activer le mode clair'
: 'Activer le mode sombre'
)
const buttonTitle = computed(() =>
isDarkMode.value
? 'Mode clair'
: 'Mode sombre'
)
const handleClick = () => {
// Ajouter la classe de transition
document.documentElement.classList.add('theme-transition')
toggleDarkMode()
// Retirer la classe apres la transition
setTimeout(() => {
document.documentElement.classList.remove('theme-transition')
}, 300)
}
</script>
<style scoped>
.theme-toggle {
position: relative;
width: 2.5rem;
height: 2.5rem;
border-radius: var(--radius-full);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
transform var(--transition-fast);
}
.theme-toggle:hover {
transform: scale(1.1);
border-color: var(--color-primary);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.theme-toggle-track {
position: relative;
width: 1.25rem;
height: 1.25rem;
}
.theme-icon {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
color: var(--color-text-primary);
transition:
transform 400ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms ease;
}
.theme-icon.sun {
transform: rotate(0deg) scale(1);
opacity: 1;
}
.theme-icon.moon {
transform: rotate(-90deg) scale(0);
opacity: 0;
}
.theme-icon.sun:not(.active) {
transform: rotate(90deg) scale(0);
opacity: 0;
}
.theme-icon.moon.active {
transform: rotate(0deg) scale(1);
opacity: 1;
}
</style>
Provide/Inject pour le Theme Global
Provider de Theme
Pour partager l’etat du theme dans toute l’application, utilisez provide/inject :
// src/providers/ThemeProvider.ts
import { provide, inject, InjectionKey, Ref, ComputedRef } from 'vue'
type ThemeMode = 'light' | 'dark' | 'system'
interface ThemeContext {
isDarkMode: Ref<boolean>
currentMode: Ref<ThemeMode>
toggleDarkMode: () => void
setMode: (mode: ThemeMode) => void
cycleMode: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export function provideTheme(context: ThemeContext) {
provide(ThemeKey, context)
}
export function useTheme(): ThemeContext {
const theme = inject(ThemeKey)
if (!theme) {
throw new Error('useTheme() must be used within a ThemeProvider')
}
return theme
}
Composant ThemeProvider
<!-- src/components/ThemeProvider.vue -->
<template>
<slot />
</template>
<script setup lang="ts">
import { useDarkMode } from '@/composables/useDarkMode'
import { provideTheme } from '@/providers/ThemeProvider'
const themeContext = useDarkMode()
provideTheme(themeContext)
</script>
Utilisation dans App.vue
<!-- src/App.vue -->
<template>
<ThemeProvider>
<div class="app-container">
<AppHeader />
<main class="main-content">
<RouterView />
</main>
<AppFooter />
</div>
</ThemeProvider>
</template>
<script setup lang="ts">
import ThemeProvider from '@/components/ThemeProvider.vue'
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'
</script>
<style>
.app-container {
min-height: 100vh;
background-color: var(--color-background);
color: var(--color-text-primary);
transition:
background-color var(--transition-normal),
color var(--transition-normal);
}
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-lg);
}
</style>
Utilisation dans les Composants Enfants
<!-- src/components/ChildComponent.vue -->
<template>
<div class="child-component">
<p>Theme actuel : {{ isDarkMode ? 'Sombre' : 'Clair' }}</p>
<button @click="toggleDarkMode">
Changer de theme
</button>
</div>
</template>
<script setup lang="ts">
import { useTheme } from '@/providers/ThemeProvider'
const { isDarkMode, toggleDarkMode } = useTheme()
</script>
Exemple Complet : Systeme de Themes
Selecteur de Theme Avance
<!-- src/components/ThemeSelector.vue -->
<template>
<div class="theme-selector" role="radiogroup" aria-label="Selection du theme">
<button
v-for="option in themeOptions"
:key="option.value"
@click="setMode(option.value)"
:class="['theme-option', { active: currentMode === option.value }]"
:aria-pressed="currentMode === option.value"
:title="option.description"
>
<component :is="option.icon" class="theme-option-icon" />
<span class="theme-option-label">{{ option.label }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTheme } from '@/providers/ThemeProvider'
// Icones en tant que composants fonctionnels
const SunIcon = {
template: `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
`
}
const MoonIcon = {
template: `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
`
}
const SystemIcon = {
template: `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
`
}
const { currentMode, setMode } = useTheme()
const themeOptions = [
{
value: 'light' as const,
label: 'Clair',
description: 'Utiliser le theme clair',
icon: SunIcon,
},
{
value: 'dark' as const,
label: 'Sombre',
description: 'Utiliser le theme sombre',
icon: MoonIcon,
},
{
value: 'system' as const,
label: 'Systeme',
description: 'Suivre les preferences du systeme',
icon: SystemIcon,
},
]
</script>
<style scoped>
.theme-selector {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-xs);
background-color: var(--color-background-secondary);
border-radius: var(--radius-lg);
}
.theme-option {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
border: none;
background-color: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition:
background-color var(--transition-fast),
color var(--transition-fast);
}
.theme-option:hover {
background-color: var(--color-surface);
color: var(--color-text-primary);
}
.theme-option.active {
background-color: var(--color-primary);
color: white;
}
.theme-option-icon {
width: 1.25rem;
height: 1.25rem;
}
.theme-option-label {
font-size: 0.75rem;
font-weight: 500;
}
</style>
Bonnes Pratiques
Voici les recommandations essentielles pour implementer un mode sombre de qualite :
1. Utilisez les CSS Variables
Les CSS variables permettent un changement de theme instantane sans re-render des composants. Definissez toutes vos couleurs comme variables et basculez-les au niveau du :root.
2. Respectez les Preferences Systeme
Detectez toujours prefers-color-scheme comme valeur par defaut. Les utilisateurs apprecient que leur preference soit automatiquement respectee.
3. Persistez le Choix Utilisateur
Une fois que l’utilisateur fait un choix explicite, sauvegardez-le dans localStorage. Sa preference doit etre prioritaire sur celle du systeme.
4. Evitez le Flash de Theme
Le “flash” qui apparait lors du chargement initial est desagreable. Ajoutez un script inline dans le <head> pour appliquer le theme avant le rendu :
<script>
(function() {
const stored = localStorage.getItem('theme-mode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const dark = stored === 'dark' || (stored === 'system' || !stored) && prefersDark;
if (dark) document.documentElement.classList.add('dark');
})();
</script>
5. Testez l’Accessibilite
Verifiez que les contrastes sont suffisants dans les deux themes. Utilisez des outils comme Lighthouse ou axe pour valider les ratios de contraste (minimum 4.5:1 pour le texte normal).
6. Ajoutez des Transitions Fluides
Les transitions entre themes doivent etre douces mais pas trop longues (200-300ms). Evitez de transitionner les proprietes couteuses comme background-image.
Pieges Courants
1. Ne Pas Gerer le SSR/SSG
En rendu cote serveur, localStorage et window ne sont pas disponibles. Verifiez toujours leur existence :
// Mauvais
const isDark = localStorage.getItem('dark') === 'true'
// Bon
const isDark = typeof localStorage !== 'undefined'
? localStorage.getItem('dark') === 'true'
: false
2. Oublier la Meta theme-color
La <meta name="theme-color"> affecte la barre d’adresse du navigateur mobile. Mettez-la a jour lors du changement de theme.
3. Utiliser des Couleurs en Dur
Evitez d’utiliser des couleurs hexadecimales directement dans les styles. Utilisez toujours les CSS variables pour permettre le theming.
/* Mauvais */
.card {
background-color: #ffffff;
color: #333333;
}
/* Bon */
.card {
background-color: var(--color-surface);
color: var(--color-text-primary);
}
4. Ignorer les Images et Icones
Les images et icones peuvent necessiter des variantes pour chaque theme. Utilisez des filtres CSS ou des sprites SVG avec currentColor.
5. Ne Pas Ecouter les Changements Systeme
L’utilisateur peut changer sa preference systeme pendant qu’il utilise votre application. Ecoutez l’evenement change sur la media query.
Conclusion
L’implementation d’un mode sombre avec Vue.js et la Composition API est un excellent exercice qui touche a plusieurs aspects du developpement frontend moderne :
- Gestion d’etat reactif avec les refs et les watchers
- Composables reutilisables pour la logique metier
- CSS moderne avec les custom properties
- Persistance avec localStorage
- Detection des preferences avec les media queries
- Injection de dependances avec provide/inject
Le systeme que nous avons construit est :
- Flexible : supporte trois modes (clair, sombre, systeme)
- Persistant : memorise les preferences utilisateur
- Reactif : repond aux changements systeme en temps reel
- Accessible : utilise les attributs ARIA et les contrastes adequats
- Performant : transitions CSS fluides sans re-render
N’hesitez pas a adapter ce systeme a vos besoins specifiques. Vous pouvez etendre le concept pour supporter plusieurs themes personnalises, ou integrer des palettes de couleurs dynamiques.
Exercice pratique : Implementez un systeme de themes multiples (pas seulement clair/sombre) avec une palette de couleurs configurable par l’utilisateur. Utilisez les concepts appris dans cet article pour creer un selecteur de theme complet avec preview en temps reel.
Cet article fait partie de notre serie sur Vue.js et la Composition API. Decouvrez nos autres articles pour approfondir vos connaissances sur le developpement d’applications Vue.js modernes.
In-Article Ad
Dev Mode
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Introduction à la Composition API Vue.js : setup(), ref() et reactive()
Découvrez la Composition API de Vue 3. Maîtrisez setup(), ref() pour les primitives et reactive() pour les objets avec des exemples pratiques.
Migrer vers la Composition API Vue 3 : Guide Etape par Etape
Migrez vos composants Vue vers la Composition API. useStore, ref, onMounted et lifecycle hooks expliques avec des exemples pratiques.
Introduction a la Composition API Vue.js : Guide Pratique avec Vuex
Apprenez a utiliser la Composition API de Vue 3 pour creer des composants maintenables. Setup, computed, refs et integration Vuex expliques.