Adopter un CSS moderne

Contenu :

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.

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,

scsscopy
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

csscopy
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 :

csscopy
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é.

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 :

scsscopy
$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 :

csscopy
: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 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 :

htmlcopy
<!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 :

htmlcopy
<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 :

htmlcopy
<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 :

htmlcopy
<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).

@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 :

scsscopy
@use 'reset';
@use 'typography';
@use 'layout';
@use 'content';

Avec le module CSS Cascading and Inheritance Level 5, CSS a cela nativement :

csscopy
@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 :

csscopy
@import url("light.css") only screen and (prefers-color-scheme: light);
@import url('dark.css') only screen and (prefers-color-scheme: dark);

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 :

scsscopy
@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 :

csscopy
:root {
  --sidebar-float: left;
}

[dir=rtl] {
  --sidebar-float: right;
}

.sidebar {
  float: var(--sidebar-float);
}

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 :

csscopy
 1@property --my-color {
 2  syntax: "<color>";
 3  inherits: false;
 4  initial-value: black;
 5}
 6
 7.primary {
 8  --my-color: red;
 9}
10
11.secondary {
12  --my-color: 10px;
13}
14
15button {
16  background-color: var(--my-color);
17  color: white;
18}

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é.

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 :

csscopy
.card { /*...*/ }
.card article { /*...*/ }
.card article header { /*...*/ }
.card article footer { /*...*/ }
.card article footer button { /*...*/ }

Avec les modules CSS imbriqués, cela peut être simplifié en :

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

csscopy
@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.