CubeCSS une méthodologie pour une CSS simple et maintenable

cssminimalismeweb

Qu’est-ce que CubeCSS ?

J’ai découvert CubeCSS, il y a environ deux ans. La lecture de la méthodologie m’a tout de suite inspiré. En voici donc mon interprétation. Je ne prétends pas suivre la méthodologie à la lettre. Je l’adapte à mes besoins et à mon style de travail. Mais je trouve que les principes de base sont très robustes.

Les Principes de CubeCSS

Le but ultime est la simplicité, en laissant le navigateur faire son travail. Des suggestions plutôt que de la micro-gestion. Cela signifie écrire moins de code spécifique.

Le meilleur code est celui qui n’existe pas du tout.

Coding Horror

Dans les entreprises avec lesquelles j’ai travaillé, j’ai surtout vu de l’atomic design. Ces deux approches sont diamétralement opposées. L’atomic design encourage la création de composants très spécifiques, avec des classes dédiées à chaque élément. CubeCSS, au contraire, incite l’utilisation de classes génériques et réutilisables.

CSS avant tout

Je commence avec les variables pour définir les couleurs, typographies et autres propriétés de base. Pour ce site, j’utilise un fichier que je nomme global.css.

:root {
  /* Colors */
  --ink: light-dark(oklch(0.2178 0 0), oklch(0.92 0 0));
  --olive: light-dark(oklch(0.4528 0.1129 130.93), oklch(0.6528 0.1129 130.93));
  --olive-ink: light-dark(oklch(0.95 0.05 130.93), oklch(0.15 0.05 130.93));
  --surface: light-dark(oklch(0.98 0.01 130.93), oklch(0.3 0 130.93));
  --background: light-dark(oklch(0.95 0.005 130.93), oklch(0.23 0 130.93));
  --softgray: light-dark(oklch(0.922 0 0), oklch(0.4178 0 0));  
  
  /* Fluid Typography */
  --step--2: clamp(0.6252rem, 0.8451rem + -0.2837vw, 0.7813rem);
  --step--1: clamp(0.884rem, 0.9594rem + -0.0972vw, 0.9375rem);
  --step-0: clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem);
  --step-1: clamp(1.35rem, 1.1792rem + 0.7591vw, 1.7675rem);
  /* ... */  
  
  /* Fluid Spacing */
  --space-3xs: clamp(0.3125rem, 0.3125rem + 0vw, 0.3125rem);
  --space-2xs: clamp(0.5625rem, 0.5369rem + 0.1136vw, 0.625rem);
  --space-xs: clamp(0.875rem, 0.8494rem + 0.1136vw, 0.9375rem);
  --space-s: clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem);
  /* ... */  
  
  /* One-up pairs */
  --space-3xs-2xs: clamp(0.3125rem, 0.1847rem + 0.5682vw, 0.625rem);
  --space-2xs-xs: clamp(0.5625rem, 0.4091rem + 0.6818vw, 0.9375rem);
  --space-xs-s: clamp(0.875rem, 0.7216rem + 0.6818vw, 1.25rem);
  --space-s-m: clamp(1.125rem, 0.8182rem + 1.3636vw, 1.875rem);
  /* ... */  
  
  /* Custom pairs */
  --space-s-xl: clamp(1.125rem, 0.0511rem + 4.7727vw, 3.75rem);
}

Pour la typographie et les espacements, j’utilise des échelles fluides. Je les génère grâce à Utopia. Une fois que j’ai ces variables, j’essaie de mettre en place les éléments de base. Par exemple, les titres, les paragraphes, les listes, etc. Je les définis dans le fichier base.css.

body {
  font-family: 'Inter', sans-serif;
  color: var(--ink);
  font-size: var(--step-0);
  background-color: var(--background);    
}
h1 {
  font-size: var(--step-4);
}
h2 {
  font-size: var(--step-3);
}
/* la même pour h3, h4, h5, h6 */

h1, h2, h3, h4, h5, h6 {
  line-height: 1;
  max-width: 20ch;
  font-family: var(--font-playfair-display);
}

p {
  line-height: 1.5;
  max-width: 65ch;
}

Le fichier n’a pas besoin d’être exhaustif. Je le complète au fur et à mesure des besoins. Les styles sont génériques intentionnellement. Je n’utilise que des sélecteurs de type. Je n’ajoute pas de classes spécifiques à ce stade.

Les classes de composition

Je m’attaque ensuite à la hiérarchie et la structure. C’est maintenant que je crée les premières classes.

.container {
  max-width: var(--grid-max-width);
  padding-inline: var(--grid-gutter);
  margin-inline: auto;
}

.flow > * + * {
  margin-top: var(--flow-space, 2em);
}

.region {
  padding-block: var(--region-space, var(--space-xl-2xl));
}

.grid {
  display: grid;
  gap: var(--grid-gutter);
  grid-template-columns: repeat(var(--grid-columns), 1fr);
}

.subgrid {
  display: grid;
  grid-template-columns: subgrid;
  gap: var(--grid-gutter);
}

[class*="span-"] {
  grid-column: span 1;
  @media (min-width: 60rem) {
    grid-column: span var(--span-columns, 1);
  }
}

.span-2 {
  --span-columns: 2;
}
.span-3 {
  --span-columns: 3;
}
/* ... */

Puis avec le HTML ça donne ça :

<main class="container flow">
  <article class="region flow">
    <h1>Title</h1>
    <p>Lorem ipsum dolor sit amet...</p>
  </article>
  <article class="region flow">
    <h2>Subtitle</h2>
    <p>Lorem ipsum dolor sit amet...</p>
  </article>
  <article class="region grid">
    <h2 class="span-12">Subtitle</h2>
    <p class="span-6">Lorem ipsum dolor sit amet...</p>
    <p class="span-6">Lorem ipsum dolor sit amet...</p>
  </article>
</main>

Ce que j’aime avec cette approche, c’est qu’on voit bien la structure en lisant le HTML. La nature prévisible des classes de composition rend le code plus facile à maintenir.

À partir de cette base, j’ai déjà une grande partie de mes pages articles de mon site qui fonctionnent. Je n’ai pas besoin de composants Prose (ou classe) pour gérer le contenu de mes articles.

Les classes utilitaires

C’est toutes les classes qui me permettent d’ajouter des modifications spécifiques à un élément. On peut utiliser TailwindCSS pour ça. Personnellement, je l’ai écrit à la main. Ça me force à réfléchir avant d’ajouter trop de classes.

.accent {
  color: var(--olive);
}

.italic {
  font-style: italic;
}

.drop-cap::first-letter {
  color: var(--olive);
  float: left;
  font-family: var(--font-playfair-display);
  font-size: 3rem;
  font-style: italic;
  line-height: 1;
  padding-right: 0.75rem;
  font-weight: 700;
}

Pour le moment mon site utilise uniquement six classes utilitaires.

Les blocs

Ce sont les éléments “complexes” réutilisables. Les boutons, les cartes, les formulaires, etc.

Une erreur que j’ai faite au début était de confondre composants JS et blocs CSS. Un bloc est un ensemble visuel. Il peut être utilisé dans plusieurs composants. Par exemple, l’apparence d’un bouton peut être un <a> ou un <button>.

.btn {
  display: inline-flex;
  align-items: center;
  padding: var(--space-xs) var(--space-s);
  background-color: var(--olive);
  color: var(--olive-ink);
  border-radius: var(--space-s);
  font-weight: 500;

  &:hover {
    background-color: oklch(from var(--olive) calc(l + 0.05) c h);
  }
}
<!-- Ces deux boutons sont visuellement identiques -->
<a href="#" class="btn">Click me</a>
<button class="btn">Click me</button>

Cette approche factorise mieux le code, en réutilisant les mêmes styles au mieux.

Les exceptions

Je n’utilise pas d’exceptions dans mon code. De ce que j’en comprends, il y a deux situations avec lesquelles elles sont utiles :

.btn {
  /* ... */
  &[data-variant="primary"] {
    background-color: var(--olive);
    color: var(--olive-ink);
  }

  &[data-loading="true"] {
      &::before {
        content: "";
        animation: spin 1s linear infinite;
        /* ... */
      }
  }
}

Quelques chiffres

J’ai rapidement mesuré les avant et après la mise en place de CubeCSS sur mon site. La différence n’est pas grande, mais cela est normal au vu de la complexité relativement faible de mon site.

MétriqueAvant CubeCSSAprès CubeCSS
Taille en Bytes de CSS~28 KB~12.7 KB
Minifié + Gzip~3.7 KB~3.4 KB
Nombre de Sélecteurs CSS346200

Néanmoins, je suis satisfait du résultat. J’ai la sensation que cette méthodologie mériterait plus d’attention dans les entreprises, car elle scale très bien. De plus elle est facile à comprendre et à mettre en place. Il faut juste ne plus penser à l’atomic design.