Skip to main content

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 .container ou .title dans chaque composant sans craindre d'impacter le reste de l'application.
  • Maintenance simplifiée : Supprimer un composant signifie supprimer son fichier .module.css sans 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 :

  1. 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;
    }
  2. 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.
  3. 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.

  1. Créez un fichier Alert.module.css avec une classe .alert (fond gris, bordure noire, padding 10px).
  2. Créez un composant Alert.jsx qui importe ces styles et les applique à une div.
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.

  1. Ajoutez trois classes dans Alert.module.css : .info (bleu), .success (vert), .error (rouge).
  2. Modifiez le composant pour accepter une prop type et 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.

  1. Créez un composant MarkdownViewer.
  2. Utilisez :global pour cibler les ul et li uniquement 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>
);
};