~8 min de lecture
Styles inline Angular : ce que ton lint CSS ne voit pas
Tu écris un composant Angular, tu colles ses styles juste en dessous du template, dans un styles: `...`. Tout est au même endroit, pas de fichier à ouvrir à côté, pas de saut de contexte. C'est confortable, et Angular l'encourage.
Puis un jour tu branches Stylelint pour faire respecter tes conventions CSS (px interdit, hex en dur banni, fonts du design system). Tu lances stylelint "src/**/*.css", c'est vert, tu es content. Sauf que ce vert ne veut rien dire : ton linter n'a rien lu. Tous tes styles vivent dans des .ts, et le glob *.css ne les voit pas.
C'est le trou dont personne ne parle. On va le regarder en face, puis décider quand l'inline est le bon choix et quand il te coûte cher.
Le problème : ton lint est vert parce qu'il ne lit rien
Prends ce composant, typique de ce que produit une base Angular :
// cta-button.ts
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
@Component({
selector: 'app-cta-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<a class="cta" [href]="href()"><ng-content /></a>`,
styles: `
.cta {
font-size: 18px; /* px sur du texte : casse le zoom accessibilite */
padding: 12px 24px; /* px partout */
color: #00204a; /* hex en dur, hors design system */
border-radius: 8px;
}
`,
})
export class CtaButton {
readonly href = input.required<string>();
}
Ce composant viole trois de tes règles CSS d'un coup : px sur font-size, px sur padding, #00204a au lieu du token. Et pourtant stylelint "src/**/*.css" passe au vert, parce que ces styles ne sont pas dans un fichier .css. Ils sont dans une chaîne de caractères, dans un .ts.
Prends la codebase de ce site, la landing Angular 21 qui sert le blog que tu lis là. Le compte est parlant : 29 composants sur 55 portent leurs styles en inline, zéro n'utilise styleUrl. Autrement dit, si tu lances Stylelint tel quel, tu lint exactement un fichier : le styles.css global, c'est-à-dire la seule feuille où tes hex et tes tokens sont légitimes. Les 29 endroits où un dev peut écrire du px à la volée ? Invisibles. Le linter protège pile la partie qui n'en a pas besoin, et ignore tout le reste.
Une nuance honnête si ta base est très Tailwind : une bonne part du styling passe par des classes utilitaires dans le template (px-6 py-2 text-dark-blue), qui sont hors périmètre Stylelint de toute façon (c'est du HTML, pas du CSS). La zone aveugle ne concerne donc que le CSS custom résiduel dans les styles:. Mais ce résiduel est bien réel : la navbar de ce repo en a 188 lignes. Tailwind réduit la surface, il ne la supprime pas.
Si tu as lu l'article sur Stylelint, c'est le prolongement direct : un linter ne protège que ce qu'il voit. Encore faut-il qu'il voie ton CSS.
Pourquoi Stylelint ne voit pas le CSS-in-TS
Rien de magique. Stylelint parse des fichiers CSS (ou SCSS, Less, etc.) avec PostCSS. Ton styles: `.cta { ... }`, pour Stylelint, c'est un fichier TypeScript : du code, pas du CSS. Il ne va pas fouiller les backticks à l'intérieur d'un décorateur @Component pour y deviner du CSS. Le glob *.css ne matche même pas le fichier, et si tu élargis à *.ts, Stylelint refuse de parser du TypeScript comme du CSS.
Donc par défaut, tout ton CSS inline est une zone aveugle. Ni Stylelint, ni ton design system tooling, ni un futur script d'audit ne le couvrent.
L'arbitrage : inline n'est pas mauvais, mal dosé oui
Avant de tout externaliser en panique : l'inline a de vraies qualités. La colocation (template + styles + logique dans un fichier) réduit le saut de contexte pour un petit composant. Et côté build, inline ou externe, c'est le même pipeline Angular : même compilation, même encapsulation, même sortie. Le choix est ergonomique et outillage, pas performance.
La ligne de décision tient en une phrase : inline tant que c'est court et local, externe dès que ça grossit ou doit être outillé.
Regarde ce que ça donne en vrai, sur ce repo :
- Un footer, ~6 lignes de styles inline. Parfait. Personne ne veut un
.cssà côté pour six règles. La zone aveugle est minuscule et sans enjeu. - Un bouton CTA, ~55 lignes de styles inline. Ça commence à peser. On est pile sur le seuil que ce projet s'est fixé (styles inline tolérés jusqu'à ~50 lignes).
- La navbar : ~188 lignes de styles inline, dans un fichier de 528 lignes. Là, c'est un god file. Le CSS noie la logique, le fichier est illisible, et ces 188 lignes de styles échappent totalement au lint. C'est exactement le genre de dérive qu'un seuil est censé empêcher.
Le seuil des ~50 lignes n'est pas un chiffre magique, c'est un garde-fou : en dessous, le confort de la colocation l'emporte ; au-dessus, le fichier devient un fourre-tout et tu as tout intérêt à sortir le CSS.
Solution 1 : externaliser au-delà du seuil
Dès qu'un composant dépasse ton seuil, tu passes en styleUrl :
// nav-bar.ts (apres extraction)
@Component({
selector: 'app-nav-bar',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './nav-bar.html',
styleUrl: './nav-bar.css',
})
export class NavBar {}
Deux bénéfices immédiats. Le fichier .ts redevient lisible (logique d'un côté, styles de l'autre). Et surtout, nav-bar.css est maintenant un fichier .css : ton glob Stylelint le matche, tes règles px/hex/fonts s'appliquent enfin dessus. Tu viens de faire rentrer 188 lignes de CSS dans le périmètre du lint sans écrire une seule règle de plus.
C'est le mouvement le plus simple, et il rend service deux fois (lisibilité + lint). Pour un gros composant, il n'y a pas à hésiter.
Solution 2 : garder l'inline mais le linter quand même
Tu tiens à la colocation, y compris sur des composants moyens ? Alors il faut apprendre à Stylelint à lire le CSS dans les backticks. Le mécanisme s'appelle custom syntax PostCSS : un parseur qui extrait le CSS des template literals avant l'analyse, activé via le champ customSyntax.
Tout le piège est dans le choix du parseur. Le plus connu, postcss-lit, s'accroche au tag css de Lit (static styles = css`...`) ; il n'extraira **pas** le styles: `...` non taggé d'Angular. Mais l'outil dédié existe : postcss-angular, un parseur PostCSS fait exactement pour les styles inline des composants Angular. Tu le scopes aux fichiers .ts via un overrides :
{
"extends": ["stylelint-config-standard"],
"rules": {
"unit-disallowed-list": [["px"], { "ignoreProperties": { "px": ["/^border/"] } }]
},
"overrides": [
{ "files": ["**/*.ts"], "customSyntax": "postcss-angular" }
]
}
Et tu élargis le glob pour inclure les .ts :
"scripts": {
"lint:css": "stylelint \"src/**/*.{css,ts}\""
}
Deux réserves honnêtes. postcss-angular (comme postcss-styled-syntax, une alternative plus générique pour template literals) est un petit package de niche : vérifie sa maintenance et sa compatibilité avec ta version d'Angular avant d'appuyer ta CI dessus. Et tu ajoutes une passe de parsing sur tous tes .ts, plus lourde qu'un simple glob .css.
Autant être direct : dans la grande majorité des cas, la solution 1 (externaliser) reste plus simple. Le CSS-in-TS lintable se justifie si tu as beaucoup de composants moyens que tu refuses d'externaliser et que tu assumes la dépendance en plus.
Le vrai piège : croire que tout va bien
Le danger n'est pas l'inline en soi. C'est le faux sentiment de sécurité. Tu as branché Stylelint, ta CI est verte, tu penses que tes conventions CSS sont tenues. En réalité, si tes styles sont majoritairement inline et que tu lint *.css, tu n'as protégé qu'une fraction du CSS du projet, souvent la moins exposée.
Le réflexe à avoir avant de te réjouir : vérifie où vit vraiment ton CSS. Un grep de styles: sur ton src te donne la réponse en deux secondes. Si le compte est élevé et que ton lint tourne sur *.css, tu as une zone aveugle proportionnelle.
Before / after
- Before : styles inline partout,
stylelint "src/**/*.css"vert. Tu crois tes conventions tenues ; en fait 29 composants sur 55 ne sont jamais lus. Les god files (188 lignes de CSS dans un.ts) noient la logique et échappent à tout. - After : les gros composants passent en
styleUrl(lisibles + lintés), les petits restent inline en assumant une zone aveugle minuscule, et si tu veux linter l'inline restant, une custom syntax PostCSS le couvre. Ton lint vert veut enfin dire quelque chose.
Récap actionnable
- Mesure ta zone aveugle :
grep -rl "styles:" srcvs les fichiers.cssréellement lintés. Un lint*.csssur un projet full-inline ne protège presque rien. - Garde l'inline pour le petit et le local (un footer de 6 lignes n'a pas besoin d'un
.cssà côté). Inline ou externe, le bundle est identique : c'est un choix d'ergonomie. - Fixe un seuil (ce projet : ~50 lignes de styles) et externalise au-delà en
styleUrl. Bonus double : lisibilité du.ts+ le CSS rentre dans le périmètre du lint. - Traque les god files : un composant avec ~200 lignes de styles inline est déjà hors de contrôle, extrais-le en priorité.
- Si tu tiens à l'inline sur des composants moyens, branche la custom syntax PostCSS
postcss-angular(faite pour les styles inline Angular ;postcss-litvise Lit et ne lira pas lestyles:non taggé) et élargis le glob aux.ts. Souvent, externaliser (solution 1) coûte moins cher.
La colocation, c'est confortable, et souvent le bon choix. Mais un confort qui rend ton CSS invisible à ton outillage n'est plus gratuit. Sache où vit ton CSS avant de faire confiance à un lint vert.