Chapitre 33 : Gestion des thèmes via les Media Queries
Utilisation de prefers-color-scheme pour le basculement automatique et l'accessibilité visuelle.
Le concept de Color Scheme Detection
1. Quoi
La détection du schéma de couleurs est une fonctionnalité CSS permettant d'adapter l'apparence d'une interface en fonction des préférences utilisateur définies au niveau du système d'exploitation (Windows, macOS, Android, iOS).
Le mécanisme repose sur la Media Query prefers-color-scheme. Elle permet de détecter si l'utilisateur a choisi un mode "Clair" (light), "Sombre" (dark), ou s'il n'a exprimé aucune préférence (no-preference).
Contrairement à un bouton de switch manuel qui stocke un choix dans le localStorage, prefers-color-scheme est une instruction déclarative : le navigateur informe le CSS de l'état du système, et le CSS réagit instantanément.
2. Pourquoi
Dans un contexte professionnel, l'implémentation d'un mode sombre n'est plus une option "bonus", mais une norme d'accessibilité (a11y) et de confort utilisateur.
- Réduction de la fatigue visuelle : En environnement peu éclairé, un fond sombre réduit l'éblouissement.
- Économie d'énergie : Sur les écrans OLED, les pixels noirs sont éteints, ce qui prolonge l'autonomie de la batterie.
- Respect du choix utilisateur : L'utilisateur configure son OS une seule fois pour toutes ses applications. Forcer un thème clair sur un système configuré en sombre est perçu comme une mauvaise expérience utilisateur (UX).
- Performance : Le basculement via Media Query est géré nativement par le moteur de rendu du navigateur, évitant ainsi des flashs de contenu non stylisé (FOUC - Flash of Unstyled Content) souvent rencontrés avec des scripts JavaScript.
3. Comment
A. Syntaxe de base
La syntaxe est simple : on enveloppe les styles spécifiques dans un bloc @media.
/* Styles par défaut (souvent le mode clair) */
body {
background-color: white;
color: black;
}
/* Styles appliqués si le système est en mode sombre */
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: white;
}
}
B. Cas concret : Architecture basée sur les Design Tokens
En production, on ne redéfinit pas chaque propriété dans la Media Query. On utilise des Custom Properties (variables CSS) pour créer des "tokens" de design. On ne change que la valeur de la variable, pas la propriété CSS.
/* 1. Définition des tokens pour le thème clair (par défaut) */
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--accent-color: #007bff;
--surface-color: #f8f9fa;
--border-color: #dee2e6;
}
/* 2. Redéfinition des tokens pour le thème sombre */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #121212;
--text-primary: #e0e0e0;
--accent-color: #3793ff;
--surface-color: #1e1e1e;
--border-color: #333333;
}
}
/* 3. Application des tokens (le code reste identique pour les deux thèmes) */
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: system-ui, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
background-color: var(--surface-color);
border: 1px solid var(--border-color);
padding: 1rem;
border-radius: 8px;
}
.btn-primary {
background-color: var(--accent-color);
color: white;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
}
C. Limitations
Bien que puissante, la Media Query prefers-color-scheme présente des limites :
- Absence de contrôle manuel : L'utilisateur ne peut pas forcer le mode sombre sur votre site si son OS est en mode clair, à moins que vous n'ajoutiez une couche JavaScript.
- Images et Médias : Les images (JPG, PNG) ne s'adaptent pas automatiquement. Il faut utiliser l'élément
<picture>ou des filtres CSS. - Complexité des contrastes : Un simple inversion de couleurs ne suffit pas. Le mode sombre nécessite souvent un ajustement des saturations et des contrastes pour rester lisible (le blanc pur
#FFFFFFsur noir pur#000000peut créer un effet de "vibration" visuelle désagréable).
4. Zone de Danger
❌ L'erreur commune : Redéfinir tous les sélecteurs Évitez de réécrire tout votre CSS dans la Media Query. Mauvais exemple :
.header { background: white; }
.footer { background: white; }
@media (prefers-color-scheme: dark) {
.header { background: black; }
.footer { background: black; }
}
C'est un cauchemar de maintenance. Si vous changez la couleur du header, vous devez le faire à deux endroits.
✅ La bonne pratique : Abstraire via des variables
Utilisez des variables sémantiques (ex: --bg-primary) et ne changez que leur valeur.
Bon exemple :
.header, .footer { background: var(--bg-primary); }
@media (prefers-color-scheme: dark) {
:root { --bg-primary: black; }
}
Flux de détection du thème système
Questions clés
1. Quelle est la différence entre prefers-color-scheme et un switch de thème en JavaScript ?
Découvrir la réponse
prefers-color-scheme est automatique et basé sur les réglages de l'OS. Il est géré nativement par le navigateur, ce qui garantit qu'il n'y a pas de flash blanc au chargement de la page. Le switch JS permet un contrôle utilisateur granulaire (ex: "Je veux mon OS en clair mais ce site en sombre") et nécessite un stockage (cookie ou localStorage).
2. Comment gérer les images pour qu'elles s'adaptent au mode sombre ?
Découvrir la réponse
On peut utiliser l'élément <picture> avec une source spécifique :
<picture>
<source srcset="dark-logo.png" media="(prefers-color-scheme: dark)">
<img src="light-logo.png" alt="Logo" />
</picture>
Ou appliquer un filtre CSS léger pour réduire la luminosité des images en mode sombre : img { filter: brightness(.8) contrast(1.2); }.
3. Que se passe-t-il si le navigateur ne supporte pas prefers-color-scheme ?
Découvrir la réponse
Le navigateur ignorera simplement le bloc @media. C'est pourquoi on définit toujours les styles "Clairs" (ou le thème par défaut) en dehors de la Media Query. C'est l'approche de la "Dégradation Élégante" (Graceful Degradation).
4. Est-il recommandé d'utiliser du noir pur (#000) pour le mode sombre ?
Découvrir la réponse
Généralement non. Le noir pur crée un contraste trop violent avec le texte blanc, ce qui peut fatiguer l'œil ou créer des effets de traînées sur certains écrans OLED. On privilégie des gris très foncés (ex: #121212 ou #1a1a1a).
Mise en pratique
Exercice 1 : Reproduction guidée
Objectif : Créer une page simple avec un titre et un paragraphe qui basculent automatiquement entre un thème "Solaire" (Jaune/Noir) et un thème "Lunaire" (Bleu nuit/Blanc).
- Thème Solaire (Défaut) : Fond
#FFF9C4, Texte#333. - Thème Lunaire (Sombre) : Fond
#1A237E, Texte#E8EAF6.
Découvrir la solution commentée
/* Définition des tokens pour le mode Solaire */
:root {
--bg-color: #FFF9C4;
--text-color: #333;
}
/* Basculement vers le mode Lunaire via Media Query */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1A237E;
--text-color: #E8EAF6;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
display: grid;
place-content: center;
height: 100vh;
margin: 0;
transition: all 0.4s ease; /* Transition douce pour le changement de thème */
}
Exercice 2 : Adaptation
Objectif : Vous avez un composant "Carte de Produit". Ajoutez-lui un état sombre en utilisant des variables CSS. La carte doit avoir une bordure et une ombre portée. L'ombre doit devenir plus sombre et plus diffuse en mode sombre pour ne pas disparaître.
HTML de départ :
<div class="product-card">
<img src="https://placehold.co/280x180" alt="Produit">
<div class="card-content">
<h3>Casque Audio Pro</h3>
<p>129,99 €</p>
<button>Ajouter au panier</button>
</div>
</div>
CSS de départ (à adapter) :
.product-card {
background-color: #ffffff;
border: 1px solid #eeeeee;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
color: #333333;
padding: 20px;
border-radius: 12px;
max-width: 300px;
}
Valeurs cibles :
- Mode clair → fond carte :
#ffffff, ombre :rgba(0,0,0,0.1) - Mode sombre → fond carte :
#2d2d2d, ombre :rgba(0,0,0,0.5)
Consigne : Transformez les valeurs codées en dur en variables CSS (--card-bg, --card-border, --card-shadow, --card-text), puis redéfinissez-les dans @media (prefers-color-scheme: dark).
Découvrir la solution commentée
:root {
--card-bg: #ffffff;
--card-border: #eeeeee;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--card-text: #333333;
}
@media (prefers-color-scheme: dark) {
:root {
--card-bg: #2d2d2d;
--card-border: #444444;
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
--card-text: #f5f5f5;
}
}
.product-card {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow: var(--card-shadow);
color: var(--card-text);
padding: 20px;
border-radius: 12px;
max-width: 300px;
}
Exercice 3 : Conception
Objectif : Créer un système de thémage complet pour un tableau de bord (Dashboard).
HTML de départ :
<body>
<aside class="sidebar">
<h2>Mon App</h2>
<nav>
<a href="#">Accueil</a>
<a href="#">Projets</a>
<a href="#">Paramètres</a>
</nav>
</aside>
<main class="main-content">
<div class="dashboard-card">
<h3 class="text-main">Statistiques</h3>
<p class="text-muted">Mise à jour aujourd'hui à 14h</p>
<button class="btn-brand">Voir le rapport</button>
</div>
<div class="dashboard-card">
<h3 class="text-main">Activité récente</h3>
<p class="text-muted">3 nouvelles entrées enregistrées</p>
</div>
</main>
</body>
Tokens sémantiques à définir :
| Token | Rôle |
|---|---|
--app-bg | Fond global de la page |
--sidebar-bg | Fond de la barre latérale |
--card-bg | Fond des cartes |
--border-color | Couleur des bordures |
--text-main | Texte principal |
--text-muted | Texte secondaire / discret |
--brand-primary | Couleur du bouton brand |
--brand-contrast | Texte sur le bouton brand |
Valeurs attendues :
| Token | Mode clair | Mode sombre |
|---|---|---|
--app-bg | #f0f2f5 | #121212 |
--sidebar-bg | #ffffff | #1e1e1e |
--card-bg | #ffffff | #242424 |
--border-color | #dcdcdc | #333333 |
--text-main | #1c1e21 | #e4e6eb |
--text-muted | #65676b | #b0b3b8 |
--brand-primary | #0056b3 (bleu contrasté sur fond clair) | #4dabf7 (bleu lumineux sur fond sombre) |
--brand-contrast | #ffffff | #121212 |
Dimensions et structure :
body:display: flex; margin: 0;.sidebar:width: 250px; height: 100vh; padding: 2rem 1.5rem;.main-content:flex: 1; padding: 2rem;.dashboard-card:padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem;.btn-brand:padding: 0.5rem 1.25rem; border: none; border-radius: 4px; font-weight: bold; cursor: pointer;
Découvrir la solution commentée
/*
Architecture de Design Tokens
On sépare les tokens de structure, de texte et d'accentuation
*/
:root {
/* Structure */
--app-bg: #f0f2f5;
--sidebar-bg: #ffffff;
--card-bg: #ffffff;
--border-color: #dcdcdc;
/* Texte */
--text-main: #1c1e21;
--text-muted: #65676b;
/* Accentuation (Brand) - Bleu contrasté pour fond clair */
--brand-primary: #0056b3;
--brand-contrast: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
/* Structure - On utilise des gris profonds plutôt que du noir pur */
--app-bg: #121212;
--sidebar-bg: #1e1e1e;
--card-bg: #242424;
--border-color: #333333;
/* Texte - On réduit légèrement la luminosité du blanc pour éviter l'éblouissement */
--text-main: #e4e6eb;
--text-muted: #b0b3b8;
/* Accentuation - Bleu plus clair/lumineux pour ressortir sur fond sombre */
--brand-primary: #4dabf7;
--brand-contrast: #121212;
}
}
/* Application */
body {
background-color: var(--app-bg);
color: var(--text-main);
margin: 0;
display: flex;
}
.sidebar {
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
width: 250px;
height: 100vh;
}
.main-content {
flex: 1;
padding: 2rem;
}
.dashboard-card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
padding: 1.5rem;
border-radius: 8px;
color: var(--text-main);
}
.muted-text {
color: var(--text-muted);
font-size: 0.9rem;
}
.btn-brand {
background-color: var(--brand-primary);
color: var(--brand-contrast);
padding: 10px 20px;
border: none;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
}