Comment Stitches m’a réconcilié avec le style inline

Depuis que je travaille dans le web, je n’ai cessé de me questionner sur la bonne façon d’écrire du style dans le web. Celle qui rend l’intégration facile et efficace, conserve la cohérence du design, est responsive et fonctionne sur tous les navigateurs, préserve l’accessibilité du site en respectant les standards W3C, est compréhensible à lire même pour les designers, et surtout qui respecte les dogmes !

Car le style a toujours été une affaire de bonnes pratiques. Les méthodologies à appliquer, la façon dont le CSS doit être structuré, les écueils à éviter… Pour répondre à la complexité du design d’une app web, nous avons besoin de règles pour pouvoir mettre de l’ordre dans ce perpétuel chaos. Règles qui sont en évolution constante. Pas étonnant, après tout, c’est le web. Au-delà des nouvelles technologies et outils qui surgissent toutes les semaines, les applications elles-mêmes montent toujours en complexité et demandent de mieux réfléchir nos méthodes d’organisation.

Je vous propose ici une réflexion sur les méthodologies modernes du style web, au travers d’un outil en particulier : Stitches. Stitches est un outil de design system Typescript que j’ai eu l’occasion d’utiliser lors de ma dernière mission, et qui m’a particulièrement convaincu. Cet article sera donc aussi une occasion pour vous de le découvrir.

Sans surprise, les Styled Components

Je ne vous apprends probablement rien, le Component est un design-pattern clé du web. Les composants sont réutilisables, leur logique et leur style sont délimités (”scoped”), et structurent efficacement le développement de contenu web. Leur application remonte à BEM, et a évolué au fil des années avec les CSS modules, les frameworks de composant JS, et bien entendu l’outil CSS-in-JS Styled Components.

Au-delà des outils, c’est une méthodologie aussi très efficace pour concevoir un design-system. Notamment au travers de l’Atomic Design, structurant les composants sur différents niveaux.

Stitches propose lui-même une implémentation de composants, très proche de celle de styled components.

Note : les exemples qui suivent utilisent React, mais Stitches peut aussi fonctionner avec n’importe quel framework

const Card = styled({
 padding: "12px",
 background: "#3C3C3C",
 borderRadius: "2px",
});

Jusqu’ici, on navigue en terrain connu. Il est temps d’introduire les atouts de Stitches !

Le premier, c’est les tokens. Les tokens sont des variables configurables dans un fichier de configuration. Ils sont ensuite réutilisables dans les styles définis avec Stitches, et ce de manière entièrement typé (on bénéficie même de l’auto-complétion de Typescript) ! Les tokens permettent ainsi d’élaborer un design système cohérent, en évitant d’utiliser des valeurs arbitraires pour plutôt s’appuyer sur les valeurs d’une guideline. L’exemple précédent devient :

const Card = styled({
 padding: "$4",
 background: "$gray600",
 borderRadius: "$xs",
});

Deuxième fonctionnalité puissante de Stitches : les variants. Il s’agit tout simplement d’attributs permettant de modifier le style d’un composant, fonctionnant comme des props. La syntaxe est rapide à prendre en main, et permet de configurer le style facilement sans ajouter de logique. Là encore, le typage est inclus.

const Card = styled({
padding: "$4",
 borderRadius: "$xs",
variants: {
color: {
light: {
background: "$gray200",
},
dark: {
       background: "$gray600",
}
}
 },
defaultVariants: {
color: "light",
}
});

Enfin, mentionnons Radix, qui bien que n’étant pas inclus dans Stitches a été pensé pour fonctionner avec. Il s’agit de composants intégrant uniquement de la logique (menus déroulant, modales, onglets…), sans aucun style. Ils sont pensés pour être accessibles, et entièrement personnalisables avec Stitches ! Une bibliothèque de composants efficaces qui peut s’intégrer n’importe où !

Stitches propose donc une très bonne solution pour les composants UI. Mais il y a un problème : la méthodologie des composants a ses limites.

Les deux utilités du style

Le style d’une application web répond à deux problématiques.

La première, c’est l’apparence : les couleurs, bordures, police, formes… Une fonction que les composants UI et l’Atomic Design remplissent parfaitement. Chaque atome ou molécule possède ses règles.

La deuxième, c’est l’agencement : la façon dont les éléments sont alignés, espacés, et la structure des pages.

Et ici tout faire au travers de composant est moins efficace. Car autant nos composants UI sont réutilisables et isolés, autant leur agencement n’est fait que de cas particuliers. Ainsi, si on se limite exclusivement aux composants, la structure du code devient vite bancale.

Il existe un composant que vous connaissez probablement, vous l’avez croisé sur plein de projets, il est dans toutes vos pages : Wrapper. Le composant ajouté à l’arrache dans un fichier juste pour avoir un alignement flex. Il a ses variantes, parfois on l’appelle Container, d‘autres fois on tente même de lui donner des surnoms un peu métier ou avec un minimum de sens… Le résultat, c’est un enchaînement de micro-composants définis n’importe où dans le code de l’application, pour un seul usage spécifique, et finissant par aboutir à ce genre de code :

<Container>
 <TopInline>
   <PaddingTopWrapper>
     <Button/>
   </PaddingTopWrapper>
   <PaddingTopWrapper>
     <Label/>
     <Separator/>
     <Link/>
   </PaddingTopWrapper>
   <PaddingTopWrapper>
     <Button/>
     <Button/>
   </PaddingTopWrapper>
 </TopInline>
 <MainWrapper>
   <Main>
     <HeaderFlex>
       <Header/>
       <IconContainer>
         <Icon>
       </IconContainer>
     </HeaderFlex>
     <Content>
       <ContentChild>
         <ContentLeftWrapper/>
         <ContentRightWrapper/>
       </ContentChild>
     </Content>
   </Main>
 </MainWrapper>
</Container>

Sauriez-vous dire lesquels de ces composants sont définis dans leurs propres fichiers ? Lesquels servent juste ici ? Lesquels sont des atomes ? Lesquels sont des métiers ? Le rôle de chacun ? Et si non seulement c’est difficile à la lecture, ça l’est encore plus à l’écriture : pour chaque petit flex ou padding que l’on veut ajouter, il faut déclarer un nouveau composant quelque part, lui trouver un nom, et revenir au template pour le placer !

Autre problème de cette approche : on perd facilement la sémantique HTML. Or celle-ci est tout de même importante pour l’accessibilité ! Les navigateurs possèdent de nombreuses fonctionnalités d’accessibilité fonctionnant par-dessus les standards du web. Les capacités d’un select, h1 ou form sont d’une richesse insoupçonnée. Ne pas contrôler la sémantique HTML dans son code, c’est perdre des fonctionnalités déjà codées nativement. Vous perdez tous vos utilisateurs claviers uniquement parce que vous avez un onClick sur une balise svg.

Bref, les composants sont supers pour l’apparence, mais pour l’agencement, on a besoin d’un autre outil.

Le retour des classes utilitaires

Les classes utilitaires, ce n’est pas nouveau. On les utilisait déjà dans le début des années 2010 avec Bootstrap, et même bien avant encore. C’est tellement ancien que ça a fini par être considéré comme une mauvaise pratique ! Avant qu’elles ne reviennent à la mode avec (entre autre) Tailwind.

Tailwind a apporté beaucoup d’améliorations aux systèmes de classes utilitaires, autant au niveau optimisation que maintenabilité. Je ne vais pas trop m’étendre dessus, ce n’est pas le cœur de cet article. L’essentiel à noter, c’est qu’à l’usage, on a fini par se rendre compte que quand on voulait aligner des éléments avec flex, plutôt que d’écrire un composant avec un style spécial, c’était au bout du compte bien plus simple d’écrire :

<div class="flex">

On gagne à la fois en fluidité à l’écriture, et même en compréhension à la lecture ! De plus, on conserve la sémantique HTML.

Bien entendu, si vous connaissez Tailwind, vous savez que cet exemple est très optimiste. Généralement, ce que l’on croise ressemble à :

<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-lg flex items-center space-x-4">

Cet exemple est directement issu de la première page de la documentation de Tailwind, suivi de la remarque :

Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” and you’re right, it’s kind of ugly. In fact it’s just about impossible to think this is a good idea the first time you see it — you have to actually try it.

Je trouve assez ironique que, dès son introduction, Tailwind soit dans une position défensive. Il se montre plutôt lucide sur le principal défaut des classes utilitaires : elles peuvent rapidement devenir très verbeuses. La mise en place de style complexe ou responsive passe par une accumulation de règles qui non seulement sont laborieuses à déchiffrer, mais parasitent également le code qui n’est pas lié au style. Les classes utilitaires sont puissantes pour l’agencement, mais moins efficaces que les styled components pour l’apparence. L’idéal est donc d’utiliser intelligemment les deux méthodes de manière complémentaire.

Comment cela se traduit-il avec Stitches ? Il n’y a pas vraiment de classes utilitaires, mais un pattern qui s’en rapproche (que l’on retrouve dans de plus en plus d’outils CSS-in-JS, tel que Chakra) : celui de Box.

Le principe, c’est d’avoir un composant Box correspondant par défaut à une div, mais pouvant aussi être utilisé comme un autre élément HTML avec du polymorphisme (souvent un attribut as). Ce composant accepte une propriété css dans laquelle on peut renseigner du style de la même manière que pour les styled components.

<Box css={{
 display:"flex",
 justifyContent: "center",
 alignItems: "center",
 gap: "$2",
}}>

Tout comme les classes utilitaires donc, cela permet de renseigner du style à la volée sans s’éparpiller dans de nouveaux composants éphémères. Grâce à l’utilisation de tokens, on préserve aussi le respect du design-system en place. Et autre avantage non négligeable par rapport à Tailwind: on utilise des propriétés CSS. Là où Tailwind force à apprendre un nouveau langage dépendant de la technologie (ce savoir ne vous servira pas ailleurs), ici il est possible d’utiliser ses connaissances en CSS, et d’en acquérir de nouvelles qui serviront sur le long terme, même quand vous n’utiliserez plus Stitches.

Enfin, tout comme les classes utilitaires, cette syntaxe est pertinente pour l’agencement tant qu’elle n’est pas trop verbeuse. Dès que l’on commence à surcharger le style sur une seule box, il vaut mieux passer par un nouveau composant. Alternativement, pour quelques cas très particuliers,, il est aussi possible d’utiliser cette syntaxe :

const formStyle = css({
 display: "flex",
 flexDirection: "column",
 gap: "$3",
});// […]<form className={formStyle()} onSubmit={onSubmit} novalidate>

L’intérêt ici est de mettre le style de côté pour mettre en avant d’autres éléments de code que l’on jugerait plus important, comme la sémantique HTML par exemple, ou des call-backs d’événements.

À ce stade, il n’est pas impossible que vous pensiez : dis-donc, cela ressemble beaucoup à du style inline ! Et c’est vrai, on ne va pas se mentir. Les justifications que Tailwind écrit dans sa documentation sont elles-même en réponse à cette critique. Car comme mentionné, les classes utilitaires étaient devenues tabous. L’argument qui revenait le plus souvent était celui de la séparation des préoccupations : on ne doit pas mélanger le style à la sémantique HTML. Que ce soit en CSS ou avec des classes, pas de width sur ma balise ! Une règle que j’ai moi-même longtemps suivi, et qui me laissait assez réticent à utiliser Tailwind ou du CSS-in-JS.

Certes, techniquement, ni Tailwind ni Stitches ne sont du style inline : le style y est délimité par des classes, optimisé, dynamique, aligné sur des grilles… On évite donc beaucoup de problèmes liés à l’écriture de CSS sur des nœuds HTML. Mais à l’usage, on applique tout de même une structure de code qui était devenu déconseillée : celle de définir le style directement dans le template. On revient en arrière sur ce qui était considéré comme une mauvaise pratique… mais c’est une bonne chose. Aucun dogme n’est absolu, il est essentiel de réfléchir aux paradigmes que l’on utilise pour les évaluer en fonction du contexte et des technologies, quitte à remettre en question des acquis.

Au final, avec du recul, la séparation des préoccupations telle qu’on l’entendait il y a dix ans (séparation HTML/CSS/JS) n’est plus pertinente avec la façon dont on conçoit des applications web aujourd’hui. Même si cette approche reste efficace pour certains types de projet, à l’heure où l’on écrit logique, templates et style dans des fichiers TSX, il me paraît vain de prétendre respecter encore les anciennes règles. D’autres structurations de projet, séparant le code en composants et en services, ont su faire leurs preuves.

Ainsi le pattern Box est à mon sens une excellente solution moderne. Il pallie au problème des styles d’agencements, tout en évitant les écueils des composants Wrapper à usage unique ou des classes cryptiques.

Il reste encore une dernière difficulté à résoudre cependant : les sélecteurs.

En défense de la cascade

CSS (Cascading Style Sheets) a été conçu avec une puissante fonctionnalité qui avait l’air d’une idée brillante au moment où elle a été inventée : la Cascade. La cascade permet de faire hériter du style à tous les éléments enfants d’un nœud. Si le fond est noir à un niveau, le texte sera blanc en dessous. Si le texte est plus gros, le texte encore plus large le sera relativement à cette taille de base grâce à em. De plus, CSS permet d’utiliser des sélecteurs sur plusieurs niveaux pour styler différemment les nœuds selon leur contexte.

Un système très ingénieux, taillé pour son époque… mais qui s’est fait des ennemis, au fur et à mesure que les contenus web gagnèrent en complexité. Trop souvent cet héritage de style crée des effets de bords imprévisibles et des conflits dans tous les sens, rendant extrêmement difficile de maintenir le code quand la moindre modification peut casser le design de façon invisible. Que ce soit par les composants ou même les classes utilitaires, les méthodes de design ont évolué vers un effacement de la cascade. Le style est délimité à un élément, point barre !

Malgré cela les sélecteurs ont encore leur intérêt. Car les éléments d’une page web existent relativement les uns aux autres, et leurs relations ont elles aussi besoin de style. S’il est en effet dangereux d’écrire des sélecteurs sur trop de niveaux, à petite échelle ils permettent de gérer certains styles de manière précise. C’est par exemple pertinent lorsque l’on utilise certains états comme :focus :focus-within, :last, etc.

.button:focus .icon { /*… */ }
.label:focus-within .description { /* … */ }
.menu *:last-child { /* … */ }

Autre problématique où les sélecteurs sont indispensables : les margins. Une bonne pratique née des dernières années est de ne plus utiliser margin sur les composants. Un composant ne devrait pas affecter ce qu’il y a autour de lui. Définir une marge extérieure fixe empêche de le réutiliser, et complexifie la modification de l’agencement. À la place, c’est au parent de définir les espaces entre ses enfants. Pour cela, l’idéal est d’utiliser la propriété gap (comme montré dans les exemples précédents) dans un flex ou grid. Mais il peut y avoir des cas où gap n’est pas applicable. Dans ces situations, ce sélecteur devient très utile :

.list > * + * {
 margin-top: 1rem; // Un enfant direct suivant un autre est séparé par un espace
}

Bref, les sélecteurs sont très utiles pour l’agencement ou la gestion d’états. Comment Stitches met-il cela en pratique ? Déjà, il reprend la syntaxe d’imbrication bien pratique des préprocesseurs CSS (qui devrait aussi arriver sur CSS) :

const Card = styled('div', {
 fontSize: "$md",
 ".footer": {
    fontSize: "$sm"
 },
 "> * + *": {
   marginTop: "$2"
}
});

Mais il va même plus loin en permettant d’utiliser les composants eux-mêmes comme sélecteurs ! Une méthode redoutablement efficace pour délimiter parfaitement le style, éviter les erreurs de syntaxe sur des classes, et même gérer intelligemment les variants.

const MenuItem = styled('a', {});const Menu = styled({
 background: "$darkBlue",
 [`${MenuItem}`]: {
    color: "$white"
 },
variants: {
   light: {
     true: {
       background: "$lightBlue",
       [`${MenuItem}`]: {
         color: "$black"
       },
     },
   }
 }
});

Enfin on peut même utiliser une imbrication ascendante, comme une sorte de cascade inversée ! Concrètement, supposons que l’on ait un composant Header, et un composant Card, et que lorsque Header est utilisé à l’intérieur d’une Card, on veut qu’il ait un style différent. On pourrait ajouter une prop à Header, mais cela voudrait dire qu’il faudrait systématiquement l’activer à la main, et il ne serait utile que dans un contexte précis, ça complexifie l’usage. On pourrait aussi avoir un composant CardHeader, mais ça ajoute une sorte de redondance, en plus de dupliquer le code et d’avoir à maintenir deux composants au lieu d’un. Enfin, on pourrait utiliser un sélecteur dans Card pour cibler Header comme ci-dessus, mais ce n’est pas à Card de définir le style de Header, et elle ne devrait pas dépendre de tous les composants qu’elle pourrait contenir. La solution idéale proposée par Stitches, c’est de spécifier directement dans Header le style à appliquer lorsqu’il se trouve dans Card.

const Header = styled('h2', {
 fontSize: '$5',
 [`${Card} &`]: {
   fontSize: '$4'
 }
});

Notez que cette sélection par composants peut aussi fonctionner avec n’importe quel composant non Stitches, en lui implémentant une fonction toString.

Bref, armé de styled components, du pattern Box, et de sélecteurs précis, il n’y a plus rien que vous ne puissiez pas styler avec aisance !

Mais rien n’est jamais parfait

Il n’existe pas de solution définitive, en particulier dans le web. Aussi il est important de connaître les limites de Stitches. La première, c’est que comme indiqué au départ, il nécessite d’être configuré manuellement pour disposer d’un bon design-system (là où des solutions comme Tailwind fournissent déjà le leur). Cela demande donc un effort rigoureux de la part des designers. Et si dans le meilleur des mondes toutes les pages d’une app web seraient alignées sur la même grille, la réalité est qu’il arrive inévitablement des exceptions qui exigent de s’écarter des variables de tokens. De même, toutes les techniques illustrées dans cet article sont pertinentes si les développeurs appliquent eux-même de bonnes pratiques de structuration.

Il faut aussi rappeler que Stitches est une solution CSS-in-JS, et que cela présente quelques contraintes. Que l’ensemble du design soit écrit dans des fichiers typescript ou javascript rend leur modification difficile par des designers surtout familiers avec CSS, exigeant ainsi un développeur intermédiaire. La compilation du style passe par celle du code, rendant une partie du développement moins fluide. Et enfin du style créé avec Stitches pour React n’est réutilisable que dans des projets React, là où du CSS pur est facilement exportable n’importe où.

Ces problèmes ne sont pas exclusifs à Stitches, aussi l’outil reste malgré cela très pertinent. Néanmoins ils sont à prendre en compte en fonction du type de projet, et présentent des pistes d’amélioration à éventuellement creuser pour les futures technologies de style.

Conclusion

Considérez un instant ce même :

Je trouve aujourd’hui cette image trompeuse, voire carrément obsolète. Elle sous-entend que le progrès est forcément linéaire (ce qui déjà anthropologiquement est une croyance fausse). Comme on a pu le constater déjà avec Tailwind, il peut nous arriver de revenir en arrière, réhabiliter certaines pratiques, et en questionner d’autres (les Styled Components s’étant avérés loin d’être sans défaut). Il est prudent de se méfier des dogmes qui se prétendent absolus. Chaque nouvelle technologie ou méthode est une voie qui peut être explorée, avec ses atouts et défauts.

Ainsi Stitches n’est pas une nouvelle révolution dans le monde du web, ni l’aboutissement de toutes les méthodes qui l’ont précédé. Néanmoins c’est un outil qui introduit de nouveaux principes très efficaces : les tokens, les variants, le pattern Box, et les sélecteurs par composants. Il a su me convaincre par son usage, et je suis à présent impatient de voir comment les autres technologies s’approprieront ces concepts, et comment le développement web évoluera en conséquence.