📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~9 min de lecture

Stylelint : arrête de reprendre tes devs sur les px en review, laisse la CI le faire

Gros projet Angular, beaucoup de devs qui tournent, et une majorité de profils back pour qui le CSS est un mal nécessaire. Résultat : à chaque PR, les mêmes remarques en review. font-size: 14px au lieu de rem. color: #00204a au lieu du token. Une couleur de marque hardcodée pour la douzième fois.

Tu peux le répéter en commentaire de review jusqu'à la fin des temps. Ou tu peux l'écrire une fois, dans une config, et laisser la CI le refuser à ta place. Le dev n'a plus rien à savoir de tes conventions : le linter lui dit quoi corriger, avec la ligne exacte, avant même que tu ouvres la PR.

C'est exactement ce que fait Stylelint. Et le déclic, pour moi, c'est venu des px.

Le problème : px casse l'accessibilité, et ce n'est pas à chaque dev de le savoir

Un dev qui écrit font-size: 16px fait ce qui semble le plus logique : 16px, c'est lisible, point. Le souci, c'est que le pixel ne respecte pas la taille de police définie par l'utilisateur. Quelqu'un qui a monté la taille par défaut de son navigateur à 20px (parce qu'il voit mal) : ton 16px reste 16px. Tu écrases son réglage d'accessibilité sans le savoir.

rem, lui, est relatif à la taille racine. 1rem = la taille de police de l'utilisateur. S'il la monte à 20px, ton 1rem devient 20px. Ton interface scale avec ses besoins au lieu de les ignorer.

/* Ce qu'on ecrit spontanement */
.card-title { font-size: 18px; }   /* fige a 18px, ignore le zoom texte */

/* Ce que tu veux */
.card-title { font-size: 1.125rem; } /* scale avec le reglage utilisateur */

Le problème n'est pas que tes devs sont mauvais : c'est une subtilité d'accessibilité qui n'a rien à voir avec la feature qu'ils livrent, et qu'aucun d'entre nous n'a envie de garder en tête à chaque déclaration. Autant la sortir des têtes et la mettre dans l'outillage.

La solution : Stylelint transforme la convention en erreur de build

Stylelint, c'est ESLint pour ton CSS. Tu installes, tu configures, tu branches sur la CI. Une règle violée = build rouge = PR bloquée. Zéro discussion.

pnpm add -D stylelint stylelint-config-standard

Config de base, .stylelintrc.json à la racine :

{
  "extends": ["stylelint-config-standard"],
  "rules": {}
}

À partir de là, chaque convention devient une ligne de config. On commence par celle qui m'a lancé.

Règle 1 : interdire px, sauf là où il a du sens

unit-disallowed-list bannit une unité. Mais tu ne veux pas l'interdire partout : 1px pour une bordure, c'est légitime, rem pour une bordure d'un pixel n'a aucun sens. D'où les exceptions par propriété.

"unit-disallowed-list": [
  ["px"],
  {
    "ignoreProperties": {
      "px": ["/^border/", "outline", "/^box-shadow/"]
    }
  }
]

px est refusé sur font-size, margin, padding, width, etc., mais toléré sur border, border-width, outline, box-shadow. Un dev qui écrit padding: 12px se prend une erreur avec le message et la ligne. Il corrige en 0.75rem, la CI passe. Tu n'as rien eu à dire.

Si tu veux être plus chirurgical et forcer rem spécifiquement sur les tailles de texte, ajoute :

"declaration-property-unit-allowed-list": {
  "font-size": ["rem"],
  "line-height": []
}

font-size n'accepte plus que rem. line-height n'accepte aucune unité (donc que des ratios sans unité, ce que tu veux pour un line-height qui scale). Deux lignes, et toute une classe d'erreurs disparaît.

Règle 2 : bannir les couleurs en dur, forcer les tokens

Même logique pour les hex hardcodés. Ton design system a des tokens (--color-dark-blue, --color-primary-yellow) ; un #00204a écrit à la main, c'est une couleur qui échappe au thème et que tu ne pourras jamais changer d'un coup.

Le réflexe, c'est color-no-hex. Piège : il interdit tout hex, y compris la définition de tes tokens (--color-dark-blue: #00204a), qui est justement le seul endroit où le hex est légitime. Tu vas te faire hurler dessus sur ta propre source de vérité.

La bonne approche cible les propriétés de couleur concrètes, pas les custom properties :

"declaration-property-value-disallowed-list": {
  "color": ["/#[0-9a-fA-F]{3,8}/"],
  "background-color": ["/#[0-9a-fA-F]{3,8}/"],
  "border-color": ["/#[0-9a-fA-F]{3,8}/"],
  "fill": ["/#[0-9a-fA-F]{3,8}/"]
}

Un color: #00204a est refusé, mais --color-dark-blue: #00204a passe (c'est une custom property, pas la propriété color). Le dev est forcé d'écrire color: var(--color-dark-blue). Ta définition de token reste la seule porte d'entrée du hex dans le projet.

Règle 3 : interdire les fonts hors design system

Ton design system impose une famille (chez nous Geist + Geist Mono, via var(--font-heading) et compagnie). Interdit : qu'un dev colle un font-family: 'Inter' piqué d'un exemple Stack Overflow.

"declaration-property-value-disallowed-list": {
  "font-family": ["/Inter/", "/Roboto/", "/Space Grotesk/"]
}

Chaque famille bannie qui apparaît dans un font-family fait échouer le build. Tu étends la liste au fil des dérives que tu constates. Résiste à la tentation d'exiger l'inverse (« tout font-family doit être un var(...) ») : ça ferait échouer la définition même de tes tokens (--font-heading: 'Geist', ...), exactement le piège des hex vu au-dessus. La liste noire est plus simple et ne mord pas ta source de vérité.

En collant les snippets : les règles 2 et 3 utilisent la même clé declaration-property-value-disallowed-list. Dans ton rules, il ne peut y en avoir qu'une. Fusionne les deux objets (color, background-color, border-color, fill et font-family) sous une seule clé, sinon le second bloc écrase le premier en silence (clé JSON dupliquée).

Règle 4 : le cas "autorisé à un seul endroit"

Chez nous, backdrop-filter (le glassmorphism) est réservé à la navbar. Partout ailleurs, interdit. Stylelint ne sait pas dire "sauf sur .apple-navbar", mais tu peux l'interdire globalement :

"property-disallowed-list": ["backdrop-filter", "-webkit-backdrop-filter"]

Et sur l'unique endroit légitime, tu assumes l'exception avec un commentaire de désactivation ciblé :

.apple-navbar {
  /* stylelint-disable-next-line property-disallowed-list */
  backdrop-filter: blur(20px);
}

L'exception devient visible et documentée dans le code, au lieu d'être une règle tacite que personne ne connaît. Tout nouveau backdrop-filter ailleurs échoue. C'est le pattern pour toute convention du type "autorisé uniquement ici". Si l'endroit légitime a plusieurs déclarations (la navbar en a une poignée entre desktop et mobile), encadre-les d'une paire /* stylelint-disable property-disallowed-list */ ... /* stylelint-enable property-disallowed-list */ plutôt que de répéter le disable-next-line sur chaque ligne.

Le piège Tailwind v4 qui casse tout au premier run

Tu configures tout ça, tu lances Stylelint sur un projet Tailwind v4, et tu te prends un mur d'erreurs at-rule-no-unknown sur @theme, @plugin, @apply, @utility, @custom-variant. Stylelint ne connaît pas les at-rules de Tailwind et les prend pour des fautes.

Ne désactive surtout pas at-rule-no-unknown complètement (tu perdrais la détection des vraies fautes de frappe genre @meida). Liste les at-rules Tailwind en exceptions :

"at-rule-no-unknown": [
  true,
  {
    "ignoreAtRules": [
      "theme", "plugin", "apply", "utility",
      "variant", "custom-variant", "source", "reference"
    ]
  }
]

Il existe aussi stylelint-config-tailwindcss qui fait ce travail, mais il a souvent un train de retard sur les nouveautés v4. La liste manuelle te rend indépendant des mises à jour du plugin. À toi de voir.

Deux angles morts avant de te lancer

1. Stylelint lint du CSS, pas du CSS-in-TS. En Angular, beaucoup d'équipes écrivent les styles de composant en template literal inline (styles: [\...`]dans le.ts). Un glob "src/**/*.css"ne verra **aucun** de ces styles : il ne matche que tes vrais fichiers.css. Deux options : externaliser les styles de composant en .css(le glob les couvre alors nativement), ou brancher une syntaxe custom PostCSS typepostcss-lit` pour que Stylelint sache lire le CSS dans les backticks. Sans ça, tu crois protéger tout le projet alors que tu ne lint que ta feuille globale. Vérifie où vit vraiment ton CSS avant de te réjouir.

2. Sur un projet existant, n'allume pas tout d'un coup. Un gros styles.css qui vit depuis deux ans viole déjà tes nouvelles règles par dizaines (des px historiques, un thème de coloration syntaxique bourré de hex...). Activer les quatre règles d'un coup = un mur de rouge sur du code légitime, et l'équipe qui débranche tout le lendemain. Procède par cran : active une règle à la fois, corrige le legacy ou neutralise-le explicitement (/* stylelint-disable */ sur un bloc tiers assumé, ex. un thème PrismJS), et fais échouer la CI seulement sur le nouveau code. Le linter est un cliquet, pas un big bang.

Où le brancher : les trois niveaux

Une règle qui ne tourne qu'en CI arrive trop tard (le dev a déjà poussé). Trois niveaux, du plus proche au plus tardif :

  1. L'éditeur. L'extension Stylelint de VS Code souligne la faute pendant qu'il tape. Feedback immédiat, le dev corrige avant même de commit.
  2. Le pre-commit. Avec husky + lint-staged, Stylelint tourne sur les fichiers stagés et bloque le commit si une règle saute. Attention à ne pas te méprendre sur --fix : il corrige le cosmétique (ordre, casse, guillemets, apportés par stylelint-config-standard), mais pas les règles de ce article. unit-disallowed-list, declaration-property-*-list et property-disallowed-list ne sont pas auto-fixables : Stylelint ne sait pas convertir 18px en 1.125rem (il ignore la root font-size, la conversion n'est pas garantie 1:1). Le pre-commit signale tôt, il ne répare pas à ta place.
"lint-staged": {
  "*.css": "stylelint --fix"
}
  1. La CI. Le filet de sécurité, celui qui bloque le merge. Un script dédié, wrappé dans ta cible de lint Nx :
"scripts": {
  "lint:css": "stylelint \"src/**/*.css\""
}

Les trois niveaux se renforcent : l'éditeur pour le feedback immédiat, le pre-commit pour arrêter la faute avant le push, la CI pour la garantie. Un dev qui n'a jamais ouvert ta doc de design system se fait guider aux trois étapes sans qu'un humain intervienne.

Before / after

  • Before : conventions CSS dans ta tête et un README que personne ne lit. Tu répètes en review "mets du rem", "utilise le token", PR après PR. Les dérives passent quand tu n'es pas là pour relire. Le design system s'effrite en silence.
  • After : chaque convention est une règle. px hors bordure, hex en dur, font interdite, backdrop-filter baladeur : build rouge, ligne exacte, message clair. Le dev corrige seul. Tes reviews parlent enfin d'architecture au lieu d'unités.

Le gain n'est pas que le temps de review. C'est que tes conventions deviennent non négociables et non oubliables, indépendantes de qui relit et de son niveau de sensibilité au front.

Récap actionnable

  1. Installe stylelint + stylelint-config-standard, config à la racine.
  2. unit-disallowed-list pour bannir px sauf bordures/ombres, + declaration-property-unit-allowed-list pour forcer font-size en rem.
  3. declaration-property-value-disallowed-list ciblé sur color/background-color/etc. pour bannir les hex en dur sans casser tes définitions de tokens.
  4. Même règle pour interdire les fonts hors design system.
  5. property-disallowed-list + stylelint-disable-next-line pour les conventions "autorisé à un seul endroit".
  6. Tailwind v4 : ignoreAtRules sur at-rule-no-unknown, ne désactive pas la règle en entier.
  7. Branche aux trois niveaux : éditeur, pre-commit (bloque tôt ; --fix ne couvre que le cosmétique), CI.

La vraie leçon dépasse le CSS : toute convention que tu répètes en review est une règle de lint qui s'ignore. Si tu la dis deux fois à la main, c'est qu'elle devrait être dans la config. Toute l'équipe y gagne : plus personne n'a à deviner tes goûts front, et toi, tu récupères tes revues.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.