Cours HTML et CSS · Module 9: CSS moderne
Stylelint - Un linter pour le CSS
Dans cette leçon6
Dans une équipe de cinq bâtisseurs, chacun écrit son CSS à sa manière : l'un colle deux fois le même sélecteur, un autre imbrique les styles sur sept niveaux. La revue de code se transforme en querelle sur des détails, et les vraies erreurs passent inaperçues. Stylelint est un outil puissant d'analyse statique du code CSS, c'est-à-dire qu'il le vérifie sans lancer la page. Il aide à maintenir la cohérence, à détecter les erreurs et à faire respecter les bonnes pratiques dans les feuilles de style.
Les bases de Stylelint
Installation et configuration
Tu installes Stylelint comme dépendance de développement du projet, avec un ensemble de règles prêt à l'emploi et des plugins facultatifs :
1# Installation de base
2npm install --save-dev stylelint stylelint-config-standard
3
4# Avec des plugins supplémentaires
5npm install --save-dev stylelint-scss stylelint-order stylelint-config-prettierLe paquet stylelint-config-standard est un ensemble de règles raisonnable pour démarrer, stylelint-scss apprend à Stylelint la syntaxe SCSS, et stylelint-order veille à l'ordre des propriétés. Le paquet stylelint-config-prettier est inutile depuis Stylelint 15.
Fichier de configuration de base .stylelintrc.json
Tu enregistres la configuration dans le fichier .stylelintrc.json. Le champ extends charge un ensemble prêt à l'emploi, plugins ajoute des plugins, et rules active, désactive ou ajuste des règles individuelles :
1{
2 "extends": [
3 "stylelint-config-standard"
4 ],
5 "plugins": [
6 "stylelint-order",
7 "stylelint-scss"
8 ],
9 "rules": {
10 "indentation": 2,
11 "string-quotes": "single",
12 "no-duplicate-selectors": true,
13 "color-hex-case": "lower",
14 "color-hex-length": "short",
15 "color-named": "never",
16 "selector-combinator-space-after": "always",
17 "selector-combinator-space-before": "always",
18 "selector-pseudo-element-colon-notation": "double",
19 "selector-pseudo-class-parentheses-space-inside": "never",
20 "selector-no-qualifying-type": true,
21 "property-no-unknown": true,
22 "keyframe-declaration-no-important": true,
23 "declaration-block-trailing-semicolon": "always",
24 "declaration-no-important": true,
25 "declaration-block-single-line-max-declarations": 1,
26 "selector-class-pattern": "^[a-z0-9\\-]+$",
27 "selector-id-pattern": "^[a-z0-9\\-]+$",
28 "custom-property-pattern": "^[a-z0-9\\-]+$",
29 "max-nesting-depth": 3,
30 "order/order": [
31 "custom-properties",
32 "declarations"
33 ],
34 "order/properties-order": [
35 "position",
36 "top",
37 "right",
38 "bottom",
39 "left",
40 "z-index",
41 "display",
42 "flex",
43 "flex-direction",
44 "flex-wrap",
45 "justify-content",
46 "align-items",
47 "width",
48 "height",
49 "margin",
50 "padding",
51 "background",
52 "color",
53 "font-family",
54 "font-size",
55 "line-height",
56 "border",
57 "border-radius",
58 "transition",
59 "transform"
60 ]
61 }
62}Remarque importante : les règles purement stylistiques, comme indentation, string-quotes, color-hex-case ou declaration-block-trailing-semicolon, ont été marquées comme obsolètes dans Stylelint 15 et supprimées dans la version 16, qui les signale comme inconnues. Supprime-les ou déplace-les vers le plugin @stylistic/stylelint-plugin, et laisse la mise en forme à Prettier. Les règles qui détectent des erreurs, comme no-duplicate-selectors, fonctionnent sans changement.
Configuration étendue pour les grands projets
Dans un projet plus grand s'ajoutent des plugins pour les conventions de nommage, l'obligation d'utiliser des variables et des règles séparées pour différents types de fichiers :
1{
2 "extends": [
3 "stylelint-config-standard",
4 "stylelint-config-prettier"
5 ],
6 "plugins": [
7 "stylelint-order",
8 "stylelint-scss",
9 "stylelint-selector-bem-pattern",
10 "stylelint-declaration-use-variable"
11 ],
12 "rules": {
13 "plugin/selector-bem-pattern": {
14 "preset": "bem",
15 "presetOptions": {
16 "namespace": "c-"
17 }
18 },
19 "sh-waqar/declaration-use-variable": [
20 [
21 "color",
22 "background-color",
23 "border-color",
24 "font-family",
25 "font-size",
26 "z-index"
27 ]
28 ],
29 "color-function-notation": "modern",
30 "alpha-value-notation": "percentage",
31 "color-hex-alpha": "never",
32 "font-family-name-quotes": "always-where-recommended",
33 "function-url-quotes": "always",
34 "import-notation": "string",
35 "keyframe-selector-notation": "percentage-unless-within-keyword-only-block",
36 "media-feature-range-notation": "context",
37 "selector-not-notation": "complex",
38 "at-rule-no-vendor-prefix": true,
39 "media-feature-name-no-vendor-prefix": true,
40 "property-no-vendor-prefix": true,
41 "selector-no-vendor-prefix": true,
42 "value-no-vendor-prefix": true,
43 "max-line-length": 120,
44 "no-empty-source": null,
45 "declaration-block-no-redundant-longhand-properties": true,
46 "shorthand-property-no-redundant-values": true,
47 "comment-empty-line-before": [
48 "always",
49 {
50 "except": ["first-nested"],
51 "ignore": ["stylelint-commands"]
52 }
53 ],
54 "custom-property-empty-line-before": [
55 "always",
56 {
57 "except": ["after-custom-property", "first-nested"],
58 "ignore": ["after-comment", "inside-single-line-block"]
59 }
60 ],
61 "declaration-empty-line-before": [
62 "always",
63 {
64 "except": ["after-declaration", "first-nested"],
65 "ignore": ["after-comment", "inside-single-line-block"]
66 }
67 ]
68 },
69 "overrides": [
70 {
71 "files": ["*.scss", "**/*.scss"],
72 "customSyntax": "postcss-scss",
73 "rules": {
74 "scss/at-rule-no-unknown": true,
75 "scss/selector-no-redundant-nesting-selector": true,
76 "scss/no-duplicate-dollar-variables": true,
77 "scss/dollar-variable-pattern": "^[a-z0-9\\-]+$",
78 "scss/percent-placeholder-pattern": "^[a-z0-9\\-]+$"
79 }
80 },
81 {
82 "files": ["*.vue", "**/*.vue"],
83 "customSyntax": "postcss-html"
84 }
85 ]
86}La section overrides applique des règles supplémentaires aux fichiers indiqués, et customSyntax permet de lire du SCSS ou des fichiers Vue. Ici aussi, supprime la règle obsolète max-line-length.
Intégration avec les outils de développement
Scripts Package.json
Tu lances Stylelint avec des scripts dans package.json, ce qui permet à toute l'équipe d'utiliser les mêmes commandes :
1{
2 "scripts": {
3 "lint:css": "stylelint \"src/**/*.{css,scss}\"",
4 "lint:css:fix": "stylelint \"src/**/*.{css,scss}\" --fix",
5 "lint:css:report": "stylelint \"src/**/*.{css,scss}\" --formatter json > stylelint-report.json",
6 "prebuild": "npm run lint:css",
7 "test:css": "stylelint \"src/**/*.{css,scss}\" --formatter tap"
8 }
9}La commande npm run lint:css se contente de vérifier les fichiers, tandis que lint:css:fix, avec l'option --fix, corrige automatiquement les erreurs que l'outil sait réparer lui-même. npm lance le script prebuild avant chaque build.
Intégration VS Code (.vscode/settings.json)
L'extension officielle Stylelint pour VS Code souligne les problèmes dès l'écriture, et ces paramètres la configurent :
1{
2 "editor.formatOnSave": true,
3 "editor.codeActionsOnSave": {
4 "source.fixAll.stylelint": true
5 },
6 "css.validate": false,
7 "less.validate": false,
8 "scss.validate": false,
9 "stylelint.validate": ["css", "postcss", "scss", "sass"],
10 "stylelint.snippet": ["css", "postcss", "scss", "sass"],
11 "stylelint.enable": true
12}Le paramètre source.fixAll.stylelint corrige le fichier à chaque enregistrement, et la désactivation de la validation CSS intégrée évite les messages en double.
Hook pre-commit avec Husky
La dernière ligne de défense consiste à vérifier les fichiers juste avant le commit, pour que du code erroné n'arrive pas dans le dépôt :
1# Installer husky et lint-staged
2npm install --save-dev husky lint-staged
3
4# package.json
5{
6 "lint-staged": {
7 "*.{css,scss}": [
8 "stylelint --fix",
9 "git add"
10 ]
11 },
12 "husky": {
13 "hooks": {
14 "pre-commit": "lint-staged"
15 }
16 }
17}C'est la notation pour les anciennes versions des outils. Avec la version actuelle de Husky, tu crées le hook avec la commande npx husky init, qui crée le fichier .husky/pre-commit, et depuis sa version 10, lint-staged ajoute lui-même les fichiers corrigés, donc git add est inutile.
Exemples de règles et leurs applications
1. Formatage et cohérence
Voyons quels problèmes le linter repère. D'abord du code avec des erreurs, puis sa version corrigée :
1/* Erreurs détectées par Stylelint */
2
3/* Indentation incorrecte */
4.component {
5margin: 0;
6 padding: 1rem;
7}
8
9/* Espaces manquants */
10.component{margin:0;padding:1rem;}
11
12/* Guillemets incorrects */
13.component {
14 font-family: "Arial";
15 background: url(image.jpg);
16}
17
18/* Couleurs incorrectes */
19.component {
20 color: RED;
21 background: #ffffff;
22 border: 1px solid #fff;
23}
24
25/* Code corrigé en suivant les règles */
26
27/* Indentation correcte */
28.component {
29 margin: 0;
30 padding: 1rem;
31}
32
33/* Espaces corrects */
34.component {
35 margin: 0;
36 padding: 1rem;
37}
38
39/* Guillemets corrects */
40.component {
41 font-family: 'Arial', sans-serif;
42 background: url('image.jpg');
43}
44
45/* Couleurs correctes */
46.component {
47 color: #f00;
48 background: #fff;
49 border: 1px solid #fff;
50}Aujourd'hui, c'est Prettier qui corrige l'indentation, mais Stylelint repère toujours, par exemple, les couleurs nommées ou l'écriture #ffffff au lieu de #fff.
2. Ordre des propriétés
Le plugin stylelint-order veille à ce que les propriétés soient toujours dans l'ordre fixé par la configuration :
1/* Ordre incorrect */
2.component {
3 color: #333;
4 position: absolute;
5 background: #fff;
6 top: 0;
7 margin: 1rem;
8 display: flex;
9 left: 0;
10 padding: 1rem;
11}
12
13/* Ordre corrigé en suivant la configuration */
14.component {
15 position: absolute;
16 top: 0;
17 left: 0;
18 display: flex;
19 margin: 1rem;
20 padding: 1rem;
21 background: #fff;
22 color: #333;
23}
24
25/* Organisation encore meilleure avec regroupement */
26.component {
27 /* Positionnement */
28 position: absolute;
29 top: 0;
30 left: 0;
31 z-index: 1;
32
33 /* Mise en page */
34 display: flex;
35 flex-direction: column;
36 justify-content: center;
37 align-items: center;
38
39 /* Modèle de boîte */
40 width: 100%;
41 height: 200px;
42 margin: 1rem;
43 padding: 1rem;
44
45 /* Visuel */
46 background: #fff;
47 color: #333;
48 border: 1px solid #ddd;
49 border-radius: 4px;
50
51 /* Typographie */
52 font-family: 'Inter', sans-serif;
53 font-size: 1rem;
54 line-height: 1.5;
55
56 /* Animation */
57 transition: all 0.3s ease;
58 transform: translateY(0);
59}Avec l'option --fix, Stylelint réordonne lui-même les déclarations. L'apparence de la page ne change pas, seule la lisibilité s'améliore.
3. Validation du motif BEM
BEM est une convention de nommage : le bloc (.card), l'élément après deux tirets bas (.card__title) et le modificateur après deux tirets (.card--featured). Le plugin BEM vérifie que les classes respectent ce modèle :
1/* Noms de classes BEM incorrects */
2.Component { } /* PascalCase */
3.component_element { } /* simple underscore */
4.component-element-modifier { } /* tiret au lieu de -- */
5.componentElement { } /* camelCase */
6
7/* Noms de classes BEM corrects */
8.component { }
9.component__element { }
10.component__element--modifier { }
11.component--variant { }
12.multi-word-component { }
13.multi-word-component__element-name { }
14.multi-word-component__element-name--modifier-name { }.component__element--modifier est donc correcte, alors que le tiret bas simple, le PascalCase et le camelCase sont erronés.
4. Règles spécifiques au SCSS
Pour le SCSS, le plugin stylelint-scss vérifie les noms des variables, les placeholders et les mixins, et la règle max-nesting-depth repère les imbrications trop profondes :
1/* SCSS incorrect */
2$primaryColor: #007bff; /* variable camelCase */
3$primary_color: #007bff; /* variable avec underscore */
4
5%button-style { /* placeholder inutilisé */
6 padding: 1rem;
7}
8
9.component {
10 .nested {
11 .deep {
12 .too {
13 .deep { /* imbrication trop profonde */
14 color: red;
15 }
16 }
17 }
18 }
19}
20
21@mixin buttonMixin() { /* mixin camelCase */
22 padding: 1rem;
23}
24
25/* SCSS correct */
26$primary-color: #007bff;
27$secondary-color: #6c757d;
28
29%button-base {
30 padding: 1rem;
31 border: none;
32 border-radius: 4px;
33 cursor: pointer;
34}
35
36.component {
37 @include button-mixin;
38
39 &__element {
40 color: $primary-color;
41
42 &--modifier {
43 background: $secondary-color;
44 }
45 }
46}
47
48@mixin button-mixin($size: medium) {
49 @extend %button-base;
50
51 @if $size == small {
52 padding: 0.5rem;
53 } @else if $size == large {
54 padding: 1.5rem;
55 }
56}Les variables et les mixins écrits en kebab-case, comme $primary-color et button-mixin, s'accordent avec les noms des propriétés CSS.
Configuration avancée
Règles personnalisées pour des projets spécifiques
Les règles qui se terminent par -pattern acceptent des expressions régulières, et les listes de valeurs autorisées permettent de restreindre le CSS aux besoins du projet :
1{
2 "rules": {
3 "comment-pattern": "^[A-Z][\\s\\S]*\\.$",
4 "custom-property-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
5 "keyframes-name-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
6 "selector-class-pattern": [
7 "^[a-z]([a-z0-9-]+)?(__([a-z0-9]+-?)+)?(--([a-z0-9]+-?)+){0,2}$",
8 {
9 "message": "Expected class selector to be kebab-case BEM format"
10 }
11 ],
12 "declaration-property-value-allowed-list": {
13 "border": ["/^0$/"],
14 "border-top": ["/^0$/"],
15 "border-right": ["/^0$/"],
16 "border-bottom": ["/^0$/"],
17 "border-left": ["/^0$/"]
18 },
19 "property-allowed-list": [
20 "position",
21 "top",
22 "right",
23 "bottom",
24 "left",
25 "z-index",
26 "display",
27 "flex",
28 "flex-direction",
29 "flex-wrap",
30 "justify-content",
31 "align-items",
32 "align-content",
33 "gap",
34 "width",
35 "height",
36 "min-width",
37 "min-height",
38 "max-width",
39 "max-height",
40 "margin",
41 "margin-top",
42 "margin-right",
43 "margin-bottom",
44 "margin-left",
45 "padding",
46 "padding-top",
47 "padding-right",
48 "padding-bottom",
49 "padding-left",
50 "background",
51 "background-color",
52 "background-image",
53 "background-size",
54 "background-position",
55 "background-repeat",
56 "color",
57 "font-family",
58 "font-size",
59 "font-weight",
60 "line-height",
61 "text-align",
62 "text-decoration",
63 "text-transform",
64 "border",
65 "border-top",
66 "border-right",
67 "border-bottom",
68 "border-left",
69 "border-radius",
70 "box-shadow",
71 "opacity",
72 "visibility",
73 "overflow",
74 "overflow-x",
75 "overflow-y",
76 "cursor",
77 "transition",
78 "transform",
79 "animation"
80 ]
81 }
82}property-allowed-list n'autorise que les propriétés énumérées, c'est donc une règle très stricte, adaptée aux design systems.
Règles axées sur les performances
Ces règles limitent la complexité des sélecteurs : le nombre d'identifiants, de classes, d'imbrications ainsi que la spécificité :
1{
2 "rules": {
3 "selector-max-id": 0,
4 "selector-max-universal": 1,
5 "selector-max-type": 2,
6 "selector-max-class": 4,
7 "selector-max-attribute": 2,
8 "selector-max-pseudo-class": 3,
9 "selector-max-compound-selectors": 4,
10 "selector-max-specificity": "0,4,0",
11 "max-nesting-depth": 3,
12 "declaration-block-no-redundant-longhand-properties": true,
13 "shorthand-property-no-redundant-values": true,
14 "no-unknown-animations": true
15 }
16}selector-max-id: 0 interdit les identifiants dans les sélecteurs, et selector-max-specificity fixe une limite supérieure.
Intégration avec les outils de build
Intégration Webpack
Tu peux aussi brancher Stylelint sur ton outil de build, et tu verras alors les erreurs dans la console pendant la compilation :
1// webpack.config.js
2const StylelintPlugin = require('stylelint-webpack-plugin');
3
4module.exports = {
5 plugins: [
6 new StylelintPlugin({
7 configFile: '.stylelintrc.json',
8 context: 'src',
9 files: '**/*.{css,scss}',
10 failOnError: false,
11 quiet: false,
12 emitErrors: true
13 })
14 ]
15};Avec failOnError: false, les erreurs sont signalées, mais n'interrompent pas le build.
Intégration Gulp
Dans les projets basés sur Gulp, le linter est une tâche à part, à côté d'une tâche qui corrige les fichiers :
1// gulpfile.js
2const gulp = require('gulp');
3const stylelint = require('gulp-stylelint');
4
5gulp.task('lint-css', function() {
6 return gulp
7 .src('src/**/*.{css,scss}')
8 .pipe(stylelint({
9 reporters: [
10 { formatter: 'string', console: true }
11 ]
12 }));
13});
14
15gulp.task('fix-css', function() {
16 return gulp
17 .src('src/**/*.{css,scss}')
18 .pipe(stylelint({
19 fix: true
20 }))
21 .pipe(gulp.dest('src'));
22});La tâche fix-css réécrit les fichiers corrigés dans le dossier src.
Intégration PostCSS
Stylelint fonctionne également comme plugin PostCSS. L'ordre des plugins compte, car le linter doit voir le code avant sa transformation :
1// postcss.config.js
2module.exports = {
3 plugins: [
4 require('stylelint')({
5 config: require('./.stylelintrc.json')
6 }),
7 require('autoprefixer'),
8 require('cssnano')({
9 preset: 'default'
10 })
11 ]
12};Stylelint vérifie la source, Autoprefixer ajoute les préfixes, et cssnano minifie le résultat à la fin.
Intégration CI/CD
GitHub Actions
Sur le serveur d'intégration continue, le lint se lance à chaque push et à chaque pull request :
1# .github/workflows/css-lint.yml
2name: CSS Lint
3
4on: [push, pull_request]
5
6jobs:
7 lint:
8 runs-on: ubuntu-latest
9
10 steps:
11 - uses: actions/checkout@v4
12
13 - name: Setup Node.js
14 uses: actions/setup-node@v4
15 with:
16 node-version: '22'
17
18 - name: Install dependencies
19 run: npm ci
20
21 - name: Run Stylelint
22 run: npm run lint:css
23
24 - name: Upload lint results
25 uses: actions/upload-artifact@v4
26 if: failure()
27 with:
28 name: stylelint-results
29 path: stylelint-report.jsonLes versions des actions changent de temps en temps, vérifie-les donc dans la documentation de GitHub Actions.
Rapports personnalisés
Grâce à l'API Node.js, tu prépares ton propre résumé, par exemple le nombre d'erreurs par règle :
1// scripts/css-report.js
2const stylelint = require('stylelint');
3const fs = require('fs');
4
5stylelint.lint({
6 files: 'src/**/*.{css,scss}',
7 formatter: 'json'
8}).then(function(resultObject) {
9 const results = JSON.parse(resultObject.output);
10
11 const summary = {
12 totalFiles: results.length,
13 totalErrors: results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0),
14 errorsByRule: {}
15 };
16
17 results.forEach(file => {
18 file.warnings.forEach(warning => {
19 const rule = warning.rule;
20 summary.errorsByRule[rule] = (summary.errorsByRule[rule] || 0) + 1;
21 });
22 });
23
24 console.log('CSS Lint Summary:');
25 console.log(`Total files analyzed: ${summary.totalFiles}`);
26 console.log(`Total errors found: ${summary.totalErrors}`);
27 console.log('Errors by rule:');
28
29 Object.entries(summary.errorsByRule)
30 .sort(([,a], [,b]) => b - a)
31 .forEach(([rule, count]) => {
32 console.log(` ${rule}: ${count}`);
33 });
34
35 fs.writeFileSync('css-lint-summary.json', JSON.stringify(summary, null, 2));
36});Le script compte les avertissements de gravité error, les regroupe par nom de règle et enregistre le rapport dans un fichier JSON.
Stylelint est un outil indispensable du développement CSS professionnel, et une configuration appropriée améliore considérablement la qualité et la maintenabilité des styles. Mon conseil : mets-le en place en quatre étapes, à savoir l'installation des paquets, le fichier .stylelintrc.json, le script lint:css dans package.json et un premier lancement. Dans la prochaine leçon, nous verrons à quoi ressemblent tes styles dans différents navigateurs.
Souviens-toi : Stylelint est le chef de chantier qui examine chaque bloc de pierre avant qu'il ne rejoigne la pyramide.
Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>:is() i :where()</title>
6 <link rel="stylesheet" href="style.css">
7</head>
8<body>
9 <main>
10 <article>
11 <h2>Article sur les pyramides</h2>
12 <p>Texte avec un <a href="#">lien</a>.</p>
13 </article>
14 <section>
15 <h2>Section sur les pharaons</h2>
16 <p>Texte avec un <a href="#">lien</a>.</p>
17 </section>
18 <nav>
19 <h2>Navigation</h2>
20 <ul>
21 <li><a href="#">Link 1</a></li>
22 <li><a href="#">Link 2</a></li>
23 </ul>
24 </nav>
25 </main>
26</body>
27</html>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que Stylelint ?
2. Que fait l'option --fix lorsqu'elle est exécutée avec Stylelint ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Mets dans l'ordre les étapes d'ajout de Stylelint à un projet :