Tous les articles

Tous les articles

Le passage à avanserv, sur un système de tokens monochrome

Comment le passage à avanserv a fait de chaque couleur un rôle sur une seule gamme, et pourquoi il a fallu corriger le test de contraste avant de s'y fier.

L'essentiel du changement d'identité ne portait pas sur la couleur. Il s'agissait de faire de chaque couleur un rôle, pour pouvoir changer le thème de toute l'application depuis un seul fichier, puis de prouver que chaque rôle restait lisible dans les deux thèmes. La nouvelle identité n'est devenue visible qu'à la fin, une fois qu'il existait un seul endroit où l'appliquer.

Les anciens noms formaient un vocabulaire#

L'interface avait deux couleurs de marque, carbon et ruby, et ce n'étaient pas seulement des couleurs. C'étaient les noms des variantes de composants : un badge, un toast ou une alerte prenait variant="ruby", des tables de styles s'en servaient comme clés, des formateurs les prenaient par défaut et des tests les vérifiaient. Les retirer revenait à renommer un vocabulaire de variantes dans 54 fichiers. J'ai gardé chaque valeur de couleur à l'identique pendant le renommage, si bien que l'application claire s'affichait pixel pour pixel comme avant et que seuls les noms bougeaient. Un changement d'identité qui modifie les noms et les valeurs dans la même étape ne permet plus de distinguer une faute de frappe d'une décision.

Des rôles au-dessus d'une seule gamme#

Les composants peignaient directement des gris slate et du blanc, donc rien ne pouvait changer de thème selon le contexte. J'ai placé une couche sémantique entre la palette et les composants : canvas, card, line, content sur trois niveaux, et ring. Chaque rôle a une valeur claire par défaut et est redéfini dans une portée .brand-dark. Comme les propriétés personnalisées CSS se propagent en cascade, bg-card vaut du blanc sur un fond clair et la surface sombre dans tout sous-arbre sombre : un composant prend le thème de la portée où il se trouve, sans prop. Environ nonante composants sont passés sur ces rôles, chaque valeur claire égale au slate qu'elle remplaçait.

Un détail de Tailwind v4 a compté ici. Il retire de :root les variables de thème qu'aucun utilitaire n'utilise, et les surfaces sombres référençaient leurs tokens via var() avant que quoi que ce soit ne les consomme : le bloc de tokens devait donc être déclaré static pour être émis.

Une fois cette jointure en place, le sélecteur de thème était peu de chose : un contrôle Système, Clair et Sombre, un store qui suit le réglage du système d'exploitation en direct, et un petit script en ligne qui applique le thème avant le premier rendu, pour qu'un rechargement en mode sombre ne flashe jamais en blanc. Le plus dur, c'étaient les neutres. Ils étaient écrits de trois façons incompatibles et ont été ramenés à une seule gamme d'encre de onze paliers, conçue en OKLCH, calée sur une seule teinte et espacée selon une courbe douce plutôt que régulière. Les paliers aux deux extrémités sont des paires de surfaces qui doivent se lire comme des élévations voisines, tandis que les tons moyens portent le texte et les bordures, qui doivent se distinguer.

Le test de contraste qui mentait#

Les ratios de contraste vivaient dans trois tableaux écrits à la main, qui se contredisaient entre eux et contredisaient la feuille de style. Je les ai remplacés par un test unitaire qui lit les tokens dans la feuille de style, résout les chaînes de var() et les couleurs relatives, et vérifie le niveau AA dans les deux thèmes. Revenir sur n'importe quelle valeur ajustée le fait échouer, ce qu'aucun tableau n'a jamais fait.

Ce test avait lui-même un bug. Il mélangeait les remplissages translucides en faisant la moyenne des canaux en lumière linéaire, alors qu'un navigateur compose dans l'espace encodé en gamma : chaque teinte translucide mesurait donc plus fort qu'elle ne s'affiche. Quatre pastilles de statut en thème clair passaient AA selon le test alors qu'elles s'affichaient à 4,44:1. Une fois l'instrument corrigé, les bordures du thème clair se sont révélées un niveau entier en dessous de leurs équivalents sombres : un séparateur mesurait 1,06:1 sur la carte, la ligne discrète avait exactement la valeur des surfaces qu'elle devait séparer, et le fond d'un champ au repos était identique à l'octet près à la page derrière lui. Le test a gagné une borne de parité entre thèmes, seule façon de dire qu'un niveau doit peser autant dans les deux.

La gamme a aussi une limite physique. À sa teinte, le gamut sRGB se resserre vers le blanc : les surfaces presque blanches ne peuvent pas porter grand-chose de la teinte bleue des tons moyens, quoi que veuille le design. Seuls les paliers qui peignent les bordures pouvaient devenir moins gris.

Une police choisie pour ses chiffres#

Les nombres en colonne tressautaient à chaque mise à jour : tailles de fichiers, horodatages, cellules d'entiers. La cause était la police. Lexend, telle qu'elle était servie, n'a pas de chiffres tabulaires, donc font-variant-numeric n'avait rien à activer. J'ai vérifié le fichier de la police avant d'agir, puis je suis passé à Spline Sans, qui a de vrais chiffres tabulaires, avec Spline Sans Mono pour le monospace, afin que le code et les identifiants partagent ses métriques.

Le logo et le nom#

L'identité avanserv définitive est arrivée deux jours après le système de tokens, et le nom comme le visuel sont passés dessus en un seul commit, parce que les tests de tokens lient l'accent, les couleurs de statut et le logo entre eux. L'accent est devenu un vert, avec un palier plus foncé pour le texte sur fond clair et le palier de la marque lui-même sur fond sombre. Les couleurs de statut fournies mesuraient 3,67:1 et 2,90:1 en texte sur les fonds presque blancs : j'ai gardé leur teinte et leur saturation et n'ai plié que la luminosité, jusqu'à la valeur la plus proche de l'originale qui passe 4,5:1 partout où elle est peinte.

Le logo est une seule boucle fermée sur un axe à 45 degrés, ouverte par des points de suspension dessinés comme un motif de tirets sur son trait. La charte demandait un logo plein sous 44 pixels, ce qui rendait plein chaque emplacement de l'application et cachait les points de suspension qui portent l'identité : le contour est donc devenu la norme, avec un trait plus épais aux petites tailles. Le nom est un mot sans hampes, tout en hauteur d'x, et il paraissait trop bas à côté du logo ; un décalage de 0,075em le centre. Le renommage s'est fait sans couche de compatibilité, si bien que les préférences enregistrées, comme le thème, ont été réinitialisées une fois, un coût accepté pour ne pas traîner d'alias morts.

Un défaut est sorti de la chaîne de génération des fichiers de marque. Les sources SVG documentaient leurs tokens par leur nom dans un commentaire d'en-tête, et un double tiret est interdit dans un commentaire XML : un navigateur qui lisait le favicon de façon stricte dessinait une vignette cassée. Les images raster étaient correctes, parce que le générateur insérait chaque SVG dans du HTML avant la capture ; le seul chemin qui compte pour les navigateurs était justement celui que rien n'exerçait. Le générateur refuse désormais une source qui n'est pas du XML bien formé.

La suite#

En septembre, l'application et le site public ont cessé de garder leur propre copie de l'identité et se sont mis à la lire dans le paquet de la marque. Les tokens sont importés du paquet au lieu d'être recopiés, et le logo est dessiné à partir de la géométrie qu'il publie. Ça a révélé ce que la copie locale avait coûté : le favicon était dessiné avec un trait de 88 contre des espaces de 107, 97 et 107 unités, ce qui laissait des encoches de 19, 9 et 19 que les extrémités arrondies refermaient, si bien que l'icône d'onglet affichait la silhouette sans les points de suspension. La version publiée, un trait de 64 contre des espaces de 116, les rouvre à 52.

La même semaine, les polices et les icônes sont passées sur l'origine même du site. Chaque page chargeait ses polices chez Google et une police d'icônes chez un hébergeur tiers, livrant l'adresse de chaque visiteur à deux acteurs que la politique de confidentialité devait nommer. Les icônes sont maintenant Hugeicons Free sous licence MIT, livrées en un seul sprite SVG, et le build du site échoue sur toute référence à un hôte extérieur à ses propres domaines : ce que promet la politique de confidentialité est vérifié par le build plutôt que retenu de mémoire.