ng'stuff

🔭 🎶 🕺

Comment gérer son CSS moderne ?

Les outils évoluent, mais le language lui-même aussi. Quid des préprocesseurs ? Tailwind est-il vraiment indispensable ? Faisons le point ensemble sur les outils pertinents à notre disposition.

Retour aux sources

Sass, Less, Stylus…​

À la vitesse où évolue le web, envisager ces technos peut nous faire sourire. Pourtant, elles ont façonné CSS tel qu’il est aujourd’hui.

Dans les grandes lignes, elles apprortent :

  • Variables statiques
  • Fonctions
  • Mixins
  • Gestion de modules
  • Et surtout: imbrication (nesting)

C’est de réutilisabilité (DRY) en général qu’il manquait cruellement au CSS des années 2010, durant l’essor du web 2 à l’ère des SPA.

Ces préprocesseurs représentaient un soulagement colossal pour les devs qui passaient leur temps à ajuster des styles qui ne répondent pas toujours qu doigt et à l’œil. Surtout à une époque où chaque navigateur avait ses propres limitations (bien plus qu’aujourd’hui).

Variables statiques

Réutiliser des valeurs : basique mais essentiel. Contrairement aux variables CSS, il ne s’agit pas de vraies variables au runtime mais de mini-macro qui remplace les valeurs en brut.

C’est pour les variables statiques que je me suis re-penché sur Sass en 2026. En fait, de nombreux frameworks CSS sont écrits en Sass et aussi distribués en CSS. C’est très malin et permet de les rendre personnalisables sans exposer des dizaines/centaines de variables au runtime, tout en permettant aussi le dynamisme des variables CSS.

DRY

Des fonctions et mixins essentiellement : des éléments de base pour réutiliser son code et avoir des fichiers plus légers. Vraiment ?

Exemple du site de Sass
sass
@mixin theme($theme: DarkGray) {
  background: $theme;
  box-shadow: 0 0 1px rgba($theme, .25);
  color: #fff;
}

.info {
  @include theme;
}
.alert {
  @include theme($theme: DarkRed);
}
.success {
  @include theme($theme: DarkGreen);
}
Résultat
css
.info {
  background: DarkGray;
  box-shadow: 0 0 1px rgba(169, 169, 169, 0.25);
  color: #fff;
}

.alert {
  background: DarkRed;
  box-shadow: 0 0 1px rgba(139, 0, 0, 0.25);
  color: #fff;
}

.success {
  background: DarkGreen;
  box-shadow: 0 0 1px rgba(0, 100, 0, 0.25);
  color: #fff;
}

C’est là que le bas blesse et ce pourquoi les préprocesseurs ne font pas l’unanimité : ils détournent la manière donc CSS est réutilisable et dissimulent la complexité aux yeux des développeurs.

Les mixins sont parfois légitimes, mais les utiliser comme n’importe quel autre fonctionnalité de CSS peut mener à des feuilles générées absolument énormes.

Imbrication

Dans la veine DRY, imbriquer est extrêmement réconfortant quand on n’y avait pas droit.

Exemple du site Sass
sass
nav {
  ul {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  li { display: inline-block; }

  a {
    display: block;
    padding: 6px 12px;
    text-decoration: none;
  }
}
Résultat
css
nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
nav li {
  display: inline-block;
}
nav a {
  display: block;
  padding: 6px 12px;
  text-decoration: none;
}

As-tu remarqué ce qui pose problème ? Faisons évoluer ça un petit peu, l’équipe design veut des liens réactifs pour la navigation de côté.

sass
nav.my-specifc-sidenav {
  li {
    display: inline-block;

    a {
      display: block;
      padding: 6px 12px;
      text-decoration: none;

      &:hover, &:focus {
        background: coral;
        color: black;
        text-decoration: underline;
      }
    }
  }
}
Résultat
css
nav.my-specifc-sidenav li {
  display: inline-block;
}
nav.my-specifc-sidenav li a {
  display: block;
  padding: 6px 12px;
  text-decoration: none;
}
nav.my-specifc-sidenav li a:hover,
nav.my-specifc-sidenav li a:focus {
  background: coral;
  color: black;
  text-decoration: underline;
}

On commence à voir beaucoup de code "invisible" généré. Sans y prêter attention, dans des applications réelles, trop combiner imbrication et sélecteurs multiples (,) peut mener à des CSS de plusieurs Mo. Minifiées..! Sans parler des soucis liés à la spécificité arbitraire.

C’est suite à ce constat que l’industrie s’est recentrée autour de CSS directement.

On a alors tenté de définir des conventions de nommage pour revenir

PostCSS

Alternative : pré-transformer CSS et lui ajouter des fonctionnalités.

Avantages
  • Un seul langage
  • Évolue avec CSS
  • Flexible

On ajoute à la carte ce qui "manque" à CSS. Et il y a du choix !

Après avoir remis le nez dans Sass, j’ai re-considéré PostCSS car je voulais essentiellement les variables statiques de mon design system au compile time. postcss-simple-vars fait cela simplement:

css
$dir: top;
$blue: #056ef0;
$column: 200px;

.menu_link {
  background: $blue;
  width: $column;
}
.menu {
  width: calc(4 * $column);
  margin-$(dir): 10px;
}
Résultat
css
.menu_link {
  background: #056ef0;
  width: 200px;
}
.menu {
  width: calc(4 * 200px);
  margin-top: 10px;
}

Exactement ce qu’il fallait ! Mais on remarque déjà quelque chose : on n’écrit plus en CSS à proprement parler. Même la coloration syntaxique commence à paniquer. Un modeste prix à payer lorsqu’on n’a pas mieux, mais important à savoir avant de s’y engager.

PostCSS est encore très utilisé y compris dans des fameworks modernes. C’est un outil mature avec un large écosystème et il restitue vraiment bien les erreurs (important).

Styled Components

À l’ère des SPA est venue l’idée de lier son CSS aux composants de son framework/lib préférée. De cette manière, on résout d’une pierre deux coup la convention de nommage et le couplage code-style.

js
const Button = styled.a<{ $primary?: boolean; }>`
  --border: ${theme.color.borderStrong};

  ${props => props.$primary ? css`
    --bg: #BF4F74;
    --fg: ${theme.color.primary};
  ` : css`
    --bg: transparent;
    --fg: ${theme.color.text};
  `}

  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 3px;
  display: inline-block;
`;

Et derrière, c’est la moulinette qui génère des classes et des composants pour en wrapper d’autres. C’est techniquement très cool et peut vraiment résoudre les soucis rencontrés en développant une grande SPA.

Toutefois on se retrouve de nouveau à ne plus vraiment écrire du CSS. Et cette fois on n’écrit même plus vraiment du JavaScript/React ! Composer les composants peut devenir complexe et on ajoute au build une couche opaque et indispensable.

D’expérience, on s’était retrouvés avec de nouvelles catégories de problèmes sans strictement enforcer de convention utile.

TailwindCSS

Celui-là est très intéressant car il inverse les rôles : le rendu HTML devient responsable du style en se reposant sur des classes utilitaires générées à partir d’un design system.

C’est très réconfortant au développement car on couple très directment le style à chaque élément (moins de context switching) et on écrit moins de code au global. De plus, pour beaucoup, totalement se passer de CSS est un soulagement car c’est une vraie expertise au final.

html
<button
  type="button"
  class="rounded-md bg-pink-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-pink-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-500"
>
  Button text
</button>
Avantages
  • Repose sur la définition d’un design system et en fournit un par défaut
  • Couple DOM et style clairement
  • Permet de se passer de CSS, même pour les media queries (taille d’écran et autres variants)
  • Permet de réutiliser des snippets de composants externes, comme ci-dessus (à condition de rester sur le design system par défaut)
  • Génère et réutilise automatiquement le code
Contreparties
  • Nécessite un outil supplémentaire et opaque au build time
  • Ne permet pas le code splitting du CSS
  • Rend plus complexe et détourne l’utilisation de CSS lui-même lorsque nécessaire
  • Alourdit considérablement les vues de rendu du DOM (classes à rallonge)

Mais surtout, un système bien pensé permet d’écrire du HTML sémantique : du HTML ne comprenant pas ou peu de classes CSS peut très bien suffire à rendre une application très stylisée. C’est exactement sur ce principe qu’a été conçu le CSS.

CSS — Mais qui est ce Pokémon ?!

À l’heure où j’écris, nous sommes en 2026 et CSS a bien évolué.

Non seulement le langage embarque lui-même beaucoup de nouvelles possibilités directement embarquées dans les navigateurs, mais surtout: il contient juste ce qu’il faut pour le rendre dynamique et l’étendre.

Absolument, bien que compression (gzip/brotli) permette dans l’absolu de s’en passer raisonnablement, pour de petites applications. Étant un peu maniaque, je préfère avoir les deux et sauver chaque octet possible !

Alors, qu’est-ce qui a changé ?

Variables

Elles identifient le mieux le CSS moderne. On peut appliquer un comportement en définissant quelques variables, globalement puis localement.

Elles permettent aussi de paramétrer des composants en shadow DOM pour leur usage le plus avancé.

Il est important de noter qu’il s’agit de variables runtime, donc de code supplémentaire à télécharger. Par défaut :)

Imbrication

Considérée comme Baseline depuis décembre 2023, il est à présent possible d’envoyer en prod des déclarations imbriquées.

css
label {
  font-family: system-ui;
  font-size: 1.25rem;

  > input {
    border: blue 2px dashed;

    &:disabled {
      background-color: dimgrey;
      color: linen;
      opacity: 1;
    }
  }
}

Contrairement aux préprocesseurs comme Sass, ce code sera réellement celui utilisé par le navigateur. Plus de génération implicite de sélecteurs à rallonge !

C’est selon moi un argument majeur pour redonner une chance au CSS.

Fonctions

Pas nouvelles mais embarquées syntaxiquement. On reconnaîtra url(), translateX(), cos()/sin(), mais aussi var(), calc(), min()/max()…​

css
.card {
  background: lightblue url("img_tree.gif") no-repeat fixed center;
  color: var(--main-fg-color);
  transform: translateX(-50%);
  width: max(300px, 50%);
}

En quoi cela nous avance pour définir notre propre logique ? L’important à remarquer c’est qu’un parser CSS reconnaîtra une fonction comme telle. Nous verrons ce qu’on peut en faire avec LightningCSS.

Macros / Mixins

La spécification CSS inclut le concept de at-rules. On reconnaîtra @charset, @import, @media, @layer…​

Donc pourquoi ne pas définir les notres ?? C’est exactement ce que fait TailwindCSS avec @apply.

css
.select2-search {
  @apply rounded border border-gray-300;
}

LightningCSS nous permettra aussi de définir les nôtres.

LightningCSS

C’est l’un des outils de la stack web morderne pour manipuler du CSS de manière simple à avancée. Basé sur son implémentation Rust, il garantit aussi de hautes performances (voir le site web)

On s’est compris : celles qui nous intéressent ici sont les fonctionnalités avancées !

Son transformateur exposant l’AST parsé et permettant de le modifier à la volée, on gagne accès à toutes sortes d’intégrations. À une condition : respecter la spec CSS. Le tout depuis une API JavaScript ! (solidement typé)

La doc associée ne paye pas de mine et démontre pourtant la puissance du transformateur. Avec de simples snippets, on gagne déjà des plugins utiles et fonctionnels.

On verra dans un article dédié ce qui me semble utile et pertinent à ajouter à son CSS.

Retour aux articles 👀