Une piste écrite 0.236fr n’est pas large de 0,236. C’est sur ce genre de détail que repose golden-grid, le composant de mise en page que je viens de publier dans la 2.6.0 d’eva-css-fluid.
Une grille à 12 colonnes est un système de division. Celle-ci est un système de proportion, et la différence se lit dans ce qu’on écrit.
Diviser ou proportionner
Dans une grille à 12 colonnes égales, on prend 4, 6 ou 8 colonnes selon ce qu’on veut faire tenir. Les proportions sont un résultat, jamais une décision. Comme toutes les colonnes se valent, il ne reste que deux façons de fabriquer une hiérarchie : centrer, ou compter.
golden-grid a quatre pistes de largeurs différentes et non interchangeables, chacune avec une fonction. Le rail (φ⁻³, 0,236) porte les métadonnées et s’aligne à droite. L’épaule (φ⁻⁴, 0,146) ne porte rien : c’est elle qui désaxe la page. Le texte (φ⁻², 0,382) est la mesure de lecture. Le débord (φ⁻³, 0,236) est là où les images partent vers la droite. Un élément ne déclare jamais une largeur : il déclare le rôle qu’il joue, et la piste lui donne sa mesure.
La géométrie tient en une construction : la page vaut 1, on lui applique la coupure dorée, puis on recoupe chacune des deux parties par la même coupure. Les quatre pistes ne sont pas quatre nombres choisis, ce sont les quatre feuilles d’un arbre à deux niveaux. La somme fait exactement 1 par la récurrence φⁿ = φⁿ⁺¹ + φⁿ⁺², et elle la fait encore après arrondi à trois décimales : 0,236 + 0,146 + 0,382 + 0,236 = 1,000. CSS ne l’exige pas, fr normalise. Sommer à 1 est une discipline de lecture : chaque nombre est directement la part de page occupée, et une erreur se voit à l’addition.
Trois conséquences, et c’est pour elles que le composant existe. La page n’a pas d’axe, elle a une progression : trois bords gauches au lieu d’un milieu, et l’œil descend en suivant des bords. Le vide est une valeur, pas un reste : l’épaule est une piste déclarée, dessinée, mesurable, et aucune classe ne la vise, donc aucun contenu ne peut la récupérer. La mesure de lecture, enfin, n’est pas réglée mais déduite. Pas un seul max-width: 65ch dans la feuille : c’est 0,382 de la composition.
La marge est une part de la page
Les marges sont dorées aussi, mais sur une autre unité, et c’est le point que j’ai mis le plus longtemps à trancher.
--gg-gutter: max(5.6%, calc((100% - 1440px) / 2)); /* 5,6% = φ⁻⁶ */
Une déclaration, deux régimes. Sous la largeur maximale, le pourcentage gagne : la marge vaut φ⁻⁶ de la page et grandit avec la fenêtre, au lieu d’être un cran d’échelle posé à la main. Au-dessus, le centrage gagne : la marge absorbe la moitié du surplus et la composition se cale. La bascule tombe à 1622 px.
Deux unités, et c’est la clé du système : la marge est une part de la page, les pistes sont des parts de la composition. Le « 1 » du nombre d’or n’est pas la fenêtre, c’est ce que les marges et les gaps laissent. À 1440 px, deux marges de 80,6 et cinq gaps de 24,1 laissent 1158,2 de composition : rail 273, épaule 169, texte 442, débord 273.
Ce qui a fixé l’exposant à φ⁻⁶ n’est pas un jugement de goût, c’est un rapport. La marge vaut alors 0,47 de l’épaule : assez large pour se lire comme une marge dessinée, assez étroite pour que l’épaule reste le vide dominant. À φ⁻⁵, le rapport monte à 0,82, les deux vides se concurrencent, et la page perd ce qui la désaxe.
Cinq gaps, et la série s’arrête là
La tentation était de prolonger : marge en φ⁻⁵, gap en φ⁻⁶. Elle ne survit pas au calcul. Il n’y a que deux marges, mais cinq gaps. Au terme suivant de la série, ils prendraient 403 px sur 1440 et la composition tomberait à 778 px. Les quatre pistes n’auraient plus de quoi exister.
Le gap n’est pas une division de la page, c’est l’air entre les pistes : il relève de l’échelle, pas de la série. Je préfère une incohérence formelle assumée à une cohérence qui vide la composition.
Elle est sans conséquence, d’ailleurs, et ça se vérifie. column-gap est prélevé avant la distribution des fr : les quatre pistes se partagent ce qui reste, leurs rapports ne bougent pas. En poussant le gap de 4 px à 103 px dans le navigateur, la composition passe de 1413 à 920 px et les fractions restent 0,2360 / 0,1460 / 0,3820 / 0,2360. Pas une décimale. C’est pourquoi le gap est le seul réglage exposé à l’exécution : il resserre la géométrie, il ne peut pas la fausser.
subgrid fait le vrai travail
C’est la pièce sans laquelle rien de tout ça ne tiendrait. grid-template-columns: subgrid hérite des pistes et de leurs noms : un bloc écrit grid-column: rail sans rien savoir de la page qui le contient. Sans lui, il faudrait recalculer les pistes en pourcentages dans chaque conteneur, c’est-à-dire réintroduire exactement la dérive qu’on cherche à éliminer. C’est donc aussi son plancher de compatibilité réel : Chrome 117, Safari 16, Firefox 71, partout depuis septembre 2023.
Deux échecs muets
Le premier tient dans deux caractères. Une piste écrite 0.236fr s’écrit en réalité minmax(auto, 0.236fr), et ce minimum automatique est la contribution min-content du contenu : une URL non sécable dans le rail élargit la piste et casse la proportion dorée. Sans erreur, sans avertissement, rien dans la console, juste une page qui n’est plus dorée. Le composant écrit donc minmax(0, …) sur ses quatre pistes.
Le second était dans l’implémentation de référence, et n’est apparu qu’à la première mise en page réelle.
.blocks > * { grid-column: main; } // le défaut du flux
.rail { grid-column: rail; } // le placement
Ces deux sélecteurs ont la même spécificité, 0,1,0. Dans cet ordre, tout va bien. Dans l’autre, le défaut écrase toutes les classes de placement, et rail, blocs larges et débords retombent silencieusement dans la colonne de texte. Le CSS reste valide, la page reste valide, la grille ne l’est plus.
Ces deux-là m’ont convaincu d’une chose : une règle de mise en page ne vaut que si elle est vérifiable. Celle du composant tient en une phrase, tout conteneur commence et finit sur une ligne de la grille, et elle se contrôle à l’exécution. On lit grid-template-columns sur le rendu, on en déduit les abscisses de ligne, on mesure chaque conteneur placé, et le rapport nomme les écarts au lieu de les compter. Le seul écart admis est le titre de page et son margin-left: -0.055em, un décalage optique pour que le dessin de la lettre touche la ligne plutôt que sa boîte.
Même raisonnement pour grid-auto-flow: row dense, qui remonte le texte sur la rangée d’un titre placé au rail. Il peut réordonner visuellement le contenu, et ça coûte en accessibilité. La contrainte qui le rend acceptable : tout bloc qui quitte la colonne de texte doit précéder, dans le DOM, ce à quoi il se rapporte.
Ce que ça coûte
Rendre les marges dorées coûte de la mesure de lecture. Elle passe de 60 à 53 signes environ à 1440 px. C’est encore dans la plage confortable, entre 45 et 75, mais c’est un arbitrage, pas un repas gratuit. $golden-grid-gutter-phi: 7 la ramène à 56 signes avec une marge de 49 px, et false rend les 60 signes en repassant la marge sur l’échelle EVA. Une ligne, dans les deux cas.
L’autre limite est de portée. C’est une grille de page : les cartes, les tableaux, les formulaires relèvent d’autre chose et le composant n’a rien à leur apprendre. Il ne prévoit pas non plus d’état intermédiaire. Sous 54 rem, les quatre pistes deviennent une, toutes les lignes nommées se rabattent sur ses deux bords, et aucune règle de placement n’est réécrite. Ajouter un bloc n’oblige jamais à écrire sa contrepartie mobile. C’est le vrai gain de maintenance, plus que la géométrie.
Le composant est optionnel et n’émet aucune règle tant qu’on ne l’active pas.
@use 'eva-css-fluid' with ($golden-grid: true);
Sans Sass, dist/golden-grid.css fait 2,1 Ko et se pose après eva.css, dont il consomme les variables.



