Chapitre 53 : CSS Modules : isolation et portée locale
Encapsulation des classes CSS pour éviter les collisions de noms et garantir l'isolation des styles.
Le problème de la portée globale
Dans un projet web classique, le CSS est par nature global. Si vous définissez une classe .button dans un fichier header.css et une autre classe .button dans footer.css, la dernière règle chargée écrasera la précédente, ou pire, les styles se mélangeront de manière imprévisible.
C'est ce qu'on appelle la collision de noms (naming collision). Pour pallier cela, la communauté a créé des conventions comme BEM (Block Element Modifier), qui force l'écriture de noms longs et uniques (ex: .header__nav-item--active). Bien que BEM soit efficace, il repose sur la discipline humaine et non sur une contrainte technique.
C'est ici qu'interviennent les CSS Modules.
CSS Modules : L'isolation technique
1. Quoi
Les CSS Modules sont un système où tous les noms de classes CSS sont rendus locaux par défaut. Contrairement au CSS traditionnel, un fichier CSS Module ne définit pas des règles globales, mais exporte un objet JavaScript où chaque clé est le nom de la classe originale et chaque valeur est un nom de classe unique généré lors de la compilation (souvent un hash).
Techniquement, il ne s'agit pas d'un nouveau langage, mais d'une étape de transformation (via PostCSS ou des loaders Webpack/Vite) qui renomme vos classes pour garantir l'unicité.
2. Pourquoi
L'utilisation des CSS Modules répond à plusieurs besoins critiques dans les architectures modernes (React, Vue, Svelte) :
- Élimination des collisions : Vous pouvez nommer vos classes
.containerou.titledans chaque composant sans craindre d'impacter le reste de l'application. - Maintenance simplifiée : Supprimer un composant signifie supprimer son fichier
.module.csssans laisser de "CSS mort" orphelin dans un fichier global géant. - Dépendances explicites : Le style est importé comme un module JS, rendant le lien entre le composant et son style transparent et traçable.
- Évolutivité : Les équipes peuvent travailler sur des composants différents sans se soucier des conventions de nommage globales.
3. Comment
A. Syntaxe de base
Pour utiliser les CSS Modules, on utilise généralement l'extension .module.css.
Fichier : Button.module.css
/* Ce nom est local au module */
.btn {
background-color: royalblue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btnPrimary {
font-weight: bold;
}
Fichier : Button.jsx (ou .tsx)
import styles from './Button.module.css';
function Button() {
// styles.btn contient la chaîne générée (ex: "Button_btn__a7x2k")
return (
<button className={`${styles.btn} ${styles.btnPrimary}`}>
Cliquez-moi
</button>
);
}
B. Cas concret : Composant Card robuste
Dans un environnement professionnel, on gère souvent des états dynamiques (actif, erreur, désactivé). Voici comment structurer un module CSS pour un composant de carte.
Fichier : Card.module.css
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.title {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: #333;
}
.content {
font-size: 1rem;
color: #666;
line-height: 1.5;
}
/* Classe pour un état spécifique */
.featured {
border-color: gold;
background-color: #fffdf0;
}
Fichier : Card.tsx
import React from 'react';
import styles from './Card.module.css';
interface CardProps {
title: string;
content: string;
isFeatured?: boolean;
}
export const Card: React.FC<CardProps> = ({ title, content, isFeatured }) => {
// On construit la liste des classes dynamiquement
const cardClasses = [
styles.card,
isFeatured ? styles.featured : ''
].join(' ').trim();
return (
<div className={cardClasses}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.content}>{content}</p>
</div>
);
};
C. Limitations et spécificités
Bien que puissants, les CSS Modules présentent quelques contraintes :
- Sélecteurs globaux : Parfois, vous voulez qu'une classe reste globale (ex: pour styliser un élément injecté par une bibliothèque tierce). On utilise alors la pseudo-classe
:global./* Cette classe sera renommée */
.localClass { color: red; }
/* Cette classe restera ".external-lib-header" dans le HTML final */
:global(.external-lib-header) {
padding: 0;
} - Complexité du DOM : Le nom des classes devient illisible dans l'inspecteur du navigateur (
_Button_btn_1a2b3), ce qui peut rendre le débogage légèrement plus lent si on ne connaît pas l'outil. - Dépendance à l'outillage : Cela ne fonctionne pas nativement dans le navigateur. Un compilateur (Vite, Webpack, Next.js) est indispensable.
4. Zone de Danger
❌ Erreur commune : Utiliser des chaînes de caractères brutes
// MAUVAIS : Le style ne sera jamais appliqué car la classe est renommée
<div className="btn">Mon bouton</div>
✅ Bonne pratique : Utiliser l'objet importé
// CORRECT : On accède à la valeur hashée via l'objet styles
<div className={styles.btn}>Mon bouton</div>
❌ Erreur commune : Tenter d'importer le CSS globalement dans un module
/* Dans Button.module.css */
@import "global.css"; /* Risque de duplication massive du CSS global dans chaque module */
✅ Bonne pratique : Importer le CSS global une seule fois à la racine de l'application (main.js ou App.jsx).
Flux de fonctionnement des CSS Modules
Questions clés
1. Quelle est la différence fondamentale entre CSS classique et CSS Modules ?
Découvrir la réponse
Le CSS classique est global : une classe .title définie n'importe où affecte tous les éléments ayant cette classe dans toute la page. Les CSS Modules rendent les classes locales par défaut en leur ajoutant un suffixe unique (hash), empêchant ainsi les collisions.
2. Comment peut-on forcer une classe à rester globale dans un fichier .module.css ?
Découvrir la réponse
On utilise le sélecteur :global(.ma-classe). Cela indique au compilateur de ne pas renommer cette classe spécifique lors de la génération du CSS final.
3. Pourquoi utiliser un objet styles en JavaScript plutôt que d'écrire le nom de la classe en dur ?
Découvrir la réponse
Parce que le nom de la classe dans le fichier CSS source (ex: .btn) n'est pas celui qui sera présent dans le navigateur (ex: .Button_btn__xYz12). L'objet styles sert de dictionnaire de correspondance (mapping) entre le nom source et le nom compilé.
4. Est-ce que les CSS Modules augmentent la taille du fichier CSS final ?
Découvrir la réponse
Non, de manière significative. Le contenu des règles CSS reste identique. Seuls les noms des sélecteurs sont légèrement plus longs. L'avantage en termes de maintenance et de suppression de code mort compense largement ce gain négligeable.
Mise en pratique
Exercice 1 : Reproduction guidée
Objectif : Créer un composant Alert simple utilisant CSS Modules.
- Créez un fichier
Alert.module.cssavec une classe.alert(fond gris, bordure noire, padding 10px). - Créez un composant
Alert.jsxqui importe ces styles et les applique à unediv.
Découvrir la solution commentée
Alert.module.css
.alert {
background-color: #f4f4f4;
border: 1px solid #333;
padding: 10px;
border-radius: 4px;
font-family: sans-serif;
}
Alert.jsx
import React from 'react';
import styles from './Alert.module.css';
export const Alert = ({ message }) => {
return (
<div className={styles.alert}>
{message}
</div>
);
};
Exercice 2 : Adaptation (États dynamiques)
Objectif : Modifier le composant Alert pour supporter trois types : info, success, et error.
- Ajoutez trois classes dans
Alert.module.css:.info(bleu),.success(vert),.error(rouge). - Modifiez le composant pour accepter une prop
typeet appliquer la classe correspondante en plus de la classe.alert.
Découvrir la solution commentée
Alert.module.css
.alert {
padding: 10px;
border-radius: 4px;
border: 1px solid transparent;
}
.info {
background-color: #e3f2fd;
color: #0d47a1;
border-color: #bbdefb;
}
.success {
background-color: #e8f5e9;
color: #1b5e20;
border-color: #c8e6c9;
}
.error {
background-color: #ffebee;
color: #b71c1c;
border-color: #ffcdd2;
}
Alert.jsx
import React from 'react';
import styles from './Alert.module.css';
export const Alert = ({ message, type = 'info' }) => {
// On récupère la classe dynamique basée sur la prop type
// styles[type] permet d'accéder à la propriété de l'objet via une variable
const typeClass = styles[type] || styles.info;
return (
<div className={`${styles.alert} ${typeClass}`}>
{message}
</div>
);
};
Exercice 3 : Conception (Intégration tierce)
Objectif : Styliser un composant qui contient un élément HTML généré par une bibliothèque externe (que vous ne pouvez pas modifier en JS).
Imaginons que vous utilisiez une bibliothèque de Markdown qui génère des balises <ul> et <li> à l'intérieur de votre composant. Vous devez styliser ces listes sans affecter les autres listes du site.
- Créez un composant
MarkdownViewer. - Utilisez
:globalpour cibler lesuletliuniquement lorsqu'ils sont enfants de votre classe locale.viewer.
Découvrir la solution commentée
MarkdownViewer.module.css
/* La classe .viewer est locale et unique */
.viewer {
padding: 20px;
background: white;
border: 1px solid #eee;
}
/*
On veut cibler les <ul> et <li> générés par la lib.
Comme on ne peut pas leur ajouter de classe JS,
on utilise :global à l'intérieur de notre scope local.
*/
.viewer :global(ul) {
list-style: square;
margin-left: 20px;
color: #444;
}
.viewer :global(li) {
margin-bottom: 8px;
font-style: italic;
}
MarkdownViewer.jsx
import React from 'react';
import styles from './MarkdownViewer.module.css';
export const MarkdownViewer = ({ content }) => {
return (
<div className={styles.viewer}>
{/*
On imagine que 'content' est du HTML généré
contenant des <ul> et <li>
*/}
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
};