bcarlin.net/content/blog/008-embracing-modern-css/index.fr.md

544 lines
15 KiB
Markdown
Raw Normal View History

---
# vim: spell spelllang=fr
title: 'Adopter un CSS moderne'
slug: '008-adopter-css-moderne'
date: '2025-07-06T14:29:26+02:00'
draft: false
categories:
- dev
tags:
- CSS
summary:
Exploration de développements récents de CSS dont j'étais passé à côté.
description: |
L'article explore les récentes évolutions du CSS, telles que l'imbrication des
règles, les variables CSS, et l'utilisation de CSS sans classes, qui
permettent une écriture plus propre et plus efficace des feuilles de style. Il
mentionne également des fonctionnalités futures comme les mixins CSS, les
propriétés personnalisées CSS, et les portées CSS, qui promettent d'améliorer
encore davantage la manière de styliser les interfaces utilisateur.
---
Je suis récemment tombé sur une note dans une page de [Plain Vanilla] dans
laquelle j'ai appris que le CSS imbriqué est valide. C'est possible depuis assez
longtemps, mais je ne le savais pas (je suis assez en retard dans mon lecteur
RSS).
Cela m'a permis de rattraper certaines des récentes évolutions du CSS.
[Plain Vanilla]: https://plainvanillaweb.com/pages/styling.html
## CSS imbriqué
L'imbrication des règles CSS est l'une des principales raisons pour lesquelles
j'ai utilisé [SASS] pendant 15 ans. J'ai toujours préféré écrire des règles
imbriquées pour regrouper des unités cohérentes de CSS. Par exemple,
```scss
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
header {
nav {
ul {
list-style: none;
li {
text-align: center;
}
}
}
}
```
a plus de sens pour moi que
```css
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header nav ul {
list-style: none;
}
header nav ul li {
text-align: center;
}
```
Il y a plusieurs raisons à ça :
* C'est plus facile à lire car les sélecteurs sont plus courts et la hiérarchie
est plus facile à comprendre ;
* Je peux déplacer un groupe de sélecteurs sans risquer d'oublier une
déclaration ;
* Je peux utiliser le repliement de code basé sur l'indentation de mon IDE pour
fermer un groupe et naviguer dans de longs fichiers CSS.
Depuis que le module [CSS Nesting] est disponible de manière générale et est
[supporté par 90% des utilisateurs], il peut être utilisé pour écrire du CSS
imbriqué. Donc maintenant, c'est du CSS valide :
```css
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
header {
nav {
ul {
list-style: none;
li {
text-align: center;
}
}
}
}
```
Le fichier [CSS de ce site] a été réécrit en utilisant le CSS imbriqué.
[SASS]: https://sass-lang.com
[CSS Nesting]: https://drafts.csswg.org/css-nesting/
[supporté par 90% des utilisateurs]: https://caniuse.com/css-nesting
[CSS de ce site]: https://bcarlin.net/static/css/bcarlin.css
## Variables CSS
Pour moi, les variables sont essentielles pour assurer une interface utilisateur
cohérente. Elles permettent de réutiliser des couleurs, des tailles, des
espacements, etc.
C'est aussi une raison pour laquelle j'ai utilisé SASS. Cela m'a permis d'écrire
du CSS avec des variables réutilisables :
```scss
$color-error: red;
$color-success: green;
label {
&.error {
color: $color-error;
}
&.success {
color: $color-success;
}
}
.notification {
&.error {
background-color: $color-error;
}
&.success {
background-color: $color-success;
}
}
```
J'ai manqué la publication du module [CSS Custom Properties for Cascading
Variables Module Level 1] de 2017, qui m'a permis d'écrire la même chose en CSS
pur :
```css
:root {
--color-error: red;
--color-success: green;
}
label {
&.error {
color: var(--color-error);
}
&.success {
color: var(--color-success);
}
}
.notification {
&.error {
background-color: var(--color-error);
}
&.success {
background-color: var(--color-success);
}
}
```
[CSS Custom Properties for Cascading Variables Module Level 1]: https://www.w3.org/TR/css-variables/
## CSS Sans Classes
Peut-être que je vais à contre-courant ici, , compte tenu de la popularité des
frameworks CSS « utilitaires » comme [TailwindCSS].
Ce point n'est pas vraiment une fonctionnalité CSS en soi, mais c'est une façon
d'écrire du CSS, où le HTML sémantiquement correct est automatiquement mis en
forme correctement. Dans une certaine mesure, il est cependant soutenu par
certains [Sélecteurs CSS Niveau 4] qui sont maintenant largement implémentés
dans les navigateurs, tels que `:has`, `:is`, `:where`, `:not`, etc.
J'utilisais habituellement [BootstrapCSS] dans mes projets parce qu'il est
complet et facile à utiliser, mais je n'ai jamais aimé la façon dont il imposait
une structure CSS relativement lourde. Pour ce site, je cherchais quelque chose
de plus léger et je suis tombé sur [PicoCSS] qui a mis en forme 90% de mon site
sans changer quoi que ce soit à mes modèles.
J'avais déjà une structure HTML de base sémantiquement significative :
```html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<header>
<nav>
<ul>
<li><a href="/">Accueil</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h1>Titre de la Page</h1>
</header>
<section>
<!-- ... -->
</section>
</article>
</main>
<footer>
<!-- ... -->
</footer>
</body>
</html>
```
Et j'aime vraiment la façon dont cela fonctionne : le contenu est mis en forme
en fonction de son balisage sémantique, et non en fonction d'une structure HTML
imposée.
Par exemple, voici le [composant Modal de Bootstrap] :
```html
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Titre de la fenêtre</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Corps de la fenêtre
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>
<button type="button" class="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>
</div>
```
Voici le [composant Modal de Tailwind Plus] :
```html
<div>
<button class="rounded-md bg-gray-950/5 px-2.5 py-1.5 text-sm font-semibold text-gray-900 hover\:bg-gray-950/10">Ouvrir le dialogue</button>
<div class="relative z-10" aria-labelledby="dialog-title" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-gray-500/75 transition-opacity" aria-hidden="true"></div>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm\:items-center sm\:p-0">
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm\:my-8 sm\:w-full sm\:max-w-lg">
<div class="bg-white px-4 pt-5 pb-4 sm\:p-6 sm\:pb-4">
<div class="sm\:flex sm\:items-start">
<div class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm\:mx-0 sm\:size-10">
<svg class="size-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<div class="mt-3 text-center sm\:mt-0 sm\:ml-4 sm\:text-left">
<h3 class="text-base font-semibold text-gray-900" id="dialog-title">Titre de la fenêtre</h3>
<div class="mt-2">
Corps de la fenêtre
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm\:flex sm\:flex-row-reverse sm\:px-6">
<button type="button" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover\:bg-red-500 sm\:ml-3 sm\:w-auto">Enregistrer</button>
<button type="button" class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-gray-300 ring-inset hover\:bg-gray-50 sm\:mt-0 sm\:w-auto">Fermer</button>
</div>
</div>
</div>
</div>
</div>
</div>
```
Comparez-les avec le [composant Modal de PicoCSS] :
```html
<dialog open>
<article>
<header>
<button aria-label="Fermer" rel="prev"></button>
<p>
<strong>Titre de la fenêtre</strong>
</p>
</header>
Corps de la fenêtre
<footer>
<button class="secondary">Fermer</button>
<button>Enregistrer</button>
</footer>
</article>
</dialog>
```
Cela fait une énorme différence en termes de simplicité, de lisibilité et
d'accessibilité (notez que les attributs ARIA sont rendus inutiles car le
balisage sémantique porte déjà cette information).
[TailwindCSS]: https://tailwindcss.com
[Sélecteurs CSS Niveau 4]: https://drafts.csswg.org/selectors-4/
[BootstrapCSS]: https://getbootstrap.com
[PicoCSS]: https://picocss.com
[composant Modal de Bootstrap]: https://getbootstrap.com/docs/4.3/components/modal/#live-demo
[composant Modal de Tailwind Plus]: https://tailwindcss.com/plus/ui-blocks/application-ui/overlays/modal-dialogs
[composant Modal de PicoCSS]: https://picocss.com/docs/modal
## `@import` pour diviser les fichiers CSS
Un dernièr point pour lequel j'aimais utiliser SASS était la possibilité de
diviser les fichiers CSS en fichiers plus petits pour les rendre plus faciles à
comprendre. Par exemple :
```scss
@use 'reset';
@use 'typography';
@use 'layout';
@use 'content';
```
Avec le module [CSS Cascading and Inheritance Level 5], CSS a cela nativement :
```css
@import url('reset.css');
@import url('typography.css');
@import url('layout.css');
@import url('content.css');
```
De ma compréhension, les fichiers CSS `@import`és sont téléchargés en parallèle,
ce qui réduit le coût d'avoir plusieurs fichiers à télécharger. Les règles
CSS `@import` ont même l'avantage d'être conditionnelles. Par exemple :
```css
@import url("light.css") only screen and (prefers-color-scheme: light);
@import url('dark.css') only screen and (prefers-color-scheme: dark);
```
[CSS Cascading and Inheritance Level 5]: https://drafts.csswg.org/css-cascade-5/
## Les évolutions que j'attends avec impatience
Voici quelques spécifications que j'ai hâte d'utiliser. Je ne les utilise pas
encore en raison du support des navigateurs ou parce que je n'en ai pas encore
eu besoin. Mais je suis impatient de les essayer.
### Mixins CSS
Les Mixins CSS sont également une fonctionnalité majeure de SASS, et favorisent
un code CSS plus propre et plus réutilisable.
CSS les aura avec le [Module des fonctions et mixins CSS], qui est encore un
brouillon, et dans lequel les mixins ne sont pas encore spécifiés.
En attendant, voici un exemple du [Guide des Mixins SASS] :
```scss
@mixin rtl($property, $ltr-value, $rtl-value) {
#{$property}: $ltr-value;
[dir=rtl] & {
#{$property}: $rtl-value;
}
}
.sidebar {
@include rtl(float, left, right);
}
```
Bien que dans certains cas, cela puisse être facilement remplacé par des
variables CSS :
```css
:root {
--sidebar-float: left;
}
[dir=rtl] {
--sidebar-float: right;
}
.sidebar {
float: var(--sidebar-float);
}
```
[Guide des Mixins SASS]: https://sass-lang.com/documentation/at-rules/mixin/#arguments
[Module des fonctions et mixins CSS]: https://www.w3.org/TR/2025/WD-css-mixins-1-20250515/
### Propriétés Personnalisées CSS
Celle-ci est une petite fonctionnalité sympa du module [CSS Properties and
Values API Level 1] qui étend les variables CSS.
Elles permettent de définir le type, la valeur initiale et la règle d'héritage
d'une variable personnalisée. Par exemple :
```css {linenos=true}
@property --my-color {
syntax: "<color>";
inherits: false;
initial-value: black;
}
.primary {
--my-color: red;
}
.secondary {
--my-color: 10px;
}
button {
background-color: var(--my-color);
color: white;
}
```
Ici, la définition de `--my-color` à la ligne 12 n'est pas valide (c'est une
longueur et non une couleur). Comme la valeur de la propriété n'est pas héritée
d'un parent, la valeur initiale sera utilisée : un `<button class="secondary">`
aura un fond noir.
Cependant, comme la propriété est définie pour être une couleur, des linters
comme [Stylelint] et [ESLint] pourront attraper de telles erreurs, en plus
d'attraper les fautes de frappe dans les valeurs ou dans le nom de la propriété.
[CSS Properties and Values API Level 1]: https://drafts.css-houdini.org/css-properties-values-api
[Stylelint]: https://stylelint.io
[ESLint]: https://eslint.org
### Scopes CSS
Celle-ci est peut-être celle que j'attends le plus. Pour styliser un composant
d'interface utilisateur, il est souvent nécessaire de cibler un élément
spécifique du composant, et de répéter les sélecteurs :
```css
.card { /*...*/ }
.card article { /*...*/ }
.card article header { /*...*/ }
.card article footer { /*...*/ }
.card article footer button { /*...*/ }
```
Avec les modules CSS imbriqués, cela peut être simplifié en :
```css
.card {
/*...*/
article {
/*...*/
header {
/*...*/
}
footer {
/*...*/
button {
/*...*/
}
}
}
}
```
Cela peut cependant avoir quelques cas particuliers et donner des résultats
inattendus (voir l'[exemple sur MDN]).
Les portées sont une nouvelle fonctionnalité du module [CSS Cascading and
Inheritance Level 6]. Elles sont une manière plus naturelle de définir des
règles :
```css
@scope (.card) {
:scope {
/*...*/
}
article {
/*...*/
header {
/*...*/
}
footer {
/*...*/
button {
/*...*/
}
}
}
}
```
La puissance de @scope vient de plusieurs faits :
* Il suit des règles de proximité : un élément est stylisé avec les règles de
portée les plus proches ;
* Il n'ajoute aucune spécificité au sélecteur, ce qui signifie qu'il peut être
remplacé plus facilement ;
* Il est plus expressif.
[CSS Cascading and Inheritance Level 6]: https://drafts.csswg.org/css-cascade-6/
[exemple sur MDN]: https://developer.mozilla.org/en-US/docs/Web/CSS/@scope#how_scope_conflicts_are_resolved