Cours HTML et CSS · Module 9: CSS moderne

Stylelint - Un linter pour le CSS

12 min de lecture
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-prettier

Le 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.json

Les 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. 1. Qu'est-ce que Stylelint ?

  2. 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 :

Articles utiles