Accueil / Articles / Où se situe la limite du cadre dans une pile d’UI réutilisable

Où se situe la limite du cadre dans une pile d’UI réutilisable

Découvrez comment les machines d’état, les composants Web ainsi que le mise en page et le mouvement basés sur des attributs permettent aux comportements d’interface utilisateur de survivre au-delà d’un framework, et quand cette portabilité n’en vaut pas la peine.

3630 mots

La plupart des équipes conçoivent très bien l’interface utilisateur pour un seul framework. Un ensemble de composants Angular soigneusement développé, un jeu de primitives React, un système de conception intégré au cycle de vie d’un seul rendeur : tout fonctionne parfaitement jusqu’à ce qu’un projet ait besoin d’un outil différent, et alors presque aucun de ces investissements ne s’avère utile. En divisant l’UI réutilisable en couches (comportement, composants rendus et petites fonctionnalités HTML telles que le mise en page et les animations), on peut décider consciemment quelles couches doivent être liées à un framework et lesquelles ne le doivent pas.

Le problème de la conception d’une UI pour un seul framework

Imaginez une équipe frontend qui passe la majeure partie de son temps avec Angular et qui apprécie l’évolution du framework : les signaux, les API modernes, ainsi qu’une grande structure pour les applications qui en ont besoin. Leur outil interne suit le modèle shadcn, où le code source des composants est copié dans chaque projet pour y être géré localement, et il s’appuie sur des primitives headless spécifiques à Angular pour les tâches d’interaction de bas niveau. À l’intérieur d’une application Angular, c’est une configuration excellente.

Les problèmes commencent lorsque le projet suivant n’est pas un projet Angular. Une application volumineuse, gérant beaucoup d’états et nécessitant de nombreuses interactions peut être parfaitement adaptée à Angular. Un site de marketing principalement statique est souvent mieux servi par Astro. Parfois, la plateforme du navigateur seule fournit déjà presque tout ce qui est nécessaire. Si c’est la technologie qui doit s’adapter aux exigences de chaque projet, et non l’inverse, alors une question subsidiaire devient inévitable :

Quelle partie de votre infrastructure UI doit survivre lorsqu’on change de framework ?

La réflexion sur cette question mène généralement à une série d’idées : d’abord les composants Web, puis les interfaces utilisateur sans interface graphique, ensuite les machines à états, et enfin une approche moins conventionnelle où le layout et les animations disposent de leurs propres attributs HTML. Au premier abord, ils semblent sans rapport. Examinés ensemble, ils se révèlent être différentes solutions à un même problème de conception : jusqu’où peut-on partager l’interface utilisateur avant que le framework d’application ne doive s’infiltrer dans chaque abstraction ?

Headless n’est pas synonyme d’agnostic vis-à-vis des frameworks

Les interfaces utilisateur sans interface graphique résolvent déjà une grande partie du problème. Radix Primitives en est une illustration claire de la raison pour laquelle ce modèle a pris de l’ampleur. Au lieu de fournir un élément Dialog avec la couleur d’arrière-plan, l’écartement, l’ombre et le rayon de bordure d’un tiers, il fournit les parties complexes et laisse la présentation à l’utilisateur.

Ces aspects difficiles représentent un véritable travail : la gestion de l’attention, la navigation au clavier, les attributs ARIA corrects, le comportement de fermeture, ainsi que toute une série de détails qui sont facilement négligés tant qu’un modal semble n’être rien de plus qu’un div superposé à un autre. Radix fournit intentionnellement ses primitives sans style et les présente comme des primitives React accessibles. Vous contrôlez la présentation, mais le modèle de composants en dessous reste celui de React.

Cela semble évident, mais cela a des conséquences. En supprimant la dépendance au style, on ne supprime pas pour autant la dépendance au framework. Il en va de même dans le monde d’Angular : une primitive peut être complètement neutre en ce qui concerne les couleurs, l’espacement et la typographie, tout en dépendant entièrement des directives Angular, des signaux, de l’injection de dépendances et des hooks de cycle de vie.

Ce n’est pas une faille. Très souvent, c’est précisément le bon choix. Une primitive conçue pour Angular peut s’intégrer étroitement à Angular, tandis qu’une primitive React peut tirer parti du modèle de composition de React. Une intégration profonde offre généralement une meilleure expérience de développement que de faire semblant que le framework n’existe pas. Le problème, c’est que deux concepts sont fréquemment considérés comme des synonymes alors qu’ils ne le sont pas :

headless
   ≠
framework-agnostic

Les composants headless éliminent l’aspect visuel. Les abstractions indépendantes du framework vont un cran plus loin en essayant d’éliminer les hypothèses concernant le rendu lui-même. Il s’agit de deux niveaux distincts de réutilisation, et il est utile de savoir lequel on a réellement besoin.

Le comportement peut exister en dehors du composant

Prenez un menu déroulant. Deux menus déroulants peuvent ne pas présenter de similitudes visuelles du tout. L’un se trouve sur une page marketing avec de gros caractères, beaucoup d’espace blanc et des transitions animées ; l’autre est intégré dans une barre d’outils d’IDE exiguë où chaque pixel compte. L’un peut être affiché par Angular, un autre par React, et un troisième par un composant Web.

Malgré ces différences visuelles, les mêmes questions reviennent sans cesse :

  • Le menu déroulant est-il actuellement ouvert ?
  • Quel élément est activé ?
  • Que fait la touche Escape ?
  • L’utilisateur peut-il naviguer entre les éléments à l’aide des flèches ?
  • Comment sont gérés les éléments désactivés ?
  • Où se porte le focus une fois le menu déroulant fermé ?

Aucune de ces questions ne dépend du fait que le markup ait été généré par Angular ou React. Il s’agit avant tout de problèmes d’interaction, et ensuite seulement de problèmes d’affichage.

Les machines à états comme contrat commun

C’est ici que l’interface utilisateur pilotée par des machines d’état devient intéressante. Zag.js est l’exemple le plus connu de cette approche. Plutôt que de considérer le composant React comme la source unique de vérité, Zag modélise les interactions sous forme de machines indépendantes du framework. Des adaptateurs spécifiques aux frameworks relient ensuite ces machines au mode de gestion de la réactivité, du cycle de vie et du DOM par React, Vue, Solid, Svelte et d’autres frameworks ; sa documentation explique également comment écrire un adaptateur pour un framework non encore couvert.

Dans un composant conventionnel, chaque aspect est regroupé au sein d’une unité spécifique à un framework :

React Dropdown
  ├─ state
  ├─ interactions
  ├─ accessibility
  └─ rendering

Avec une machine intermédiaire, le comportement est défini une seule fois et chaque rendeur l’utilise :

Dropdown behavior
                    │
               state machine
                    │
       ┌────────────┼────────────┐
       ↓            ↓            ↓
     React         Vue         Svelte

Les composants finaux restent des composants distincts, et chaque framework continue de se comporter comme tel. Ce qui a été déplacé, c’est le contrat de comportement, une définition plus utile pour ce qui est indépendant des frameworks que l’idée fantaisiste d’un composant magique qui fonctionnerait partout sans aucun travail d’intégration. Une meilleure formule serait : écrire le comportement une seule fois, puis l’adapter au rendeur qui convient à l’application.

Un composant peut exister en tant que comportement avant d’exister en tant qu’interface utilisateur

La même idée fonctionne au sein d’une bibliothèque de composants Web. Imaginez une bibliothèque construite avec Stencil qui contient les boutons, dialogues, listes déroulantes et cartes habituels. Ces composants n’ont pas besoin d’être remplacés par des machines à états. Au lieu de cela, une couche distincte peut se trouver en dessous d’eux.

Une usine de boutons connaît les états désactivé et en chargement, la gestion des clics, ainsi que les propriétés qui doivent être appliquées à l’élément interactif. Une usine de listes déroulantes connaît les opérations d’ouverture, de fermeture, de sélection et la navigation au clavier. Une usine de dialogues connaît son cycle de vie et ses règles d’interaction. Aucune de ces logiques n’a besoin de décider à quoi ressemble le composant.

Conceptuellement, quelque chose comme cela peut exister avant même que toute décision de rendu ne soit prise :

const button = createButton({
  disabled: false,
  loading: false,
  onClick(event) {
    // application behavior
  },
});

Remarquez ce qui manque : pas de Stencil, pas d’Angular, pas de composant React, même pas de CSS. La factory n’est qu’une description du comportement, et un rendeur peut être ajouté ultérieurement. Le bouton Stencil de la bibliothèque, par exemple, utilise ce comportement et expose un élément personnalisé stylisé :

<and-button variant="destructive">
  Delete
</and-button>

Une application différente peut entourer ce même noyau comportemental avec un bouton complètement différent. Pensez à une IDE de type bureau dont l’interface est intentionnellement bien plus dense qu’un site web typique. Ses boutons utilisent d’autres dimensions, d’autres tokens de conception et un langage visuel différent, donc importer le bouton de composant Web polyvalent serait une mauvaise décision. L’IDE dispose simplement de son propre bouton Angular, et ce bouton Angular peut toujours appeler la même factory createButton().

C’est là l’avantage pratique. L’objectif n’est pas celui-ci :

ONE BUTTON
    ↓
use everywhere

L’objectif est le suivant :

shared behavior
                     │
          ┌──────────┴──────────┐
          ↓                     ↓
   Web Component          Angular component
          ↓                     ↓
      Web UI                 IDE UI

La présentation reste limitée à chaque produit, tandis que la logique d’interaction fastidieuse n’a plus besoin d’être réécrite pour chacun d’eux.

Les Web Components rendent le composant affiché portable

Les machines à états rendent le comportement portable. Elles n’ont aucun impact sur l’interface utilisateur affichée, et c’est là que les Web Components conservent leur valeur. Un élément personnalisé comme celui ci-dessous appartient à la plateforme Web et non à Angular, React ou Vue :

<and-button>
  Save
</and-button>

Angular peut le rendre, Astro peut l’émettre, React peut l’utiliser, et une page HTML ordinaire peut le inclure avec une balise script. Le framework qui l’entoure peut changer tandis que l’élément reste exactement le même.

En pratique, le support des éléments personnalisés par les frameworks diffère en détails. Angular nécessite CUSTOM_ELEMENTS_SCHEMA (ou un équivalent) pour accepter des balises inconnues, tandis que React a historiquement transmis des valeurs en tant qu’attributs plutôt qu’en tant que propriétés, ce qui compliquait la gestion des données complexes et des événements personnalisés, bien que les versions récentes aient amélioré la situation. Il est utile de vérifier le support actuel des éléments personnalisés par votre framework avant d’adopter cette approche.

On obtient ainsi deux types distincts de réutilisation. Le premier concerne le comportement portable :

State machine
    ↓
portable behavior

Le second concerne un composant rendu portable :

Web Component
    ↓
portable rendered component

Parfois, on souhaite l’ensemble du composant Web. Si le bouton d’un système de conception doit avoir exactement le même aspect et se comporter de la même manière dans plusieurs applications, l’emballer en tant qu’élément personnalisé est un choix judicieux. D’autres fois, on ne veut pas du tout le composant complet. Le cas de l’IDE est précisément celui-ci : conserver la logique d’interaction, mais laisser l’application gérer entièrement le rendu et le design.

Ces architectures ne sont pas concurrentes ; elles définissent simplement la frontière d’abstraction en des endroits différents. Penser en termes de frontières plutôt que de bibliothèques permet également de comprendre une autre chose : tout élément UI réutilisable n’a pas nécessairement besoin d’être un composant.

Le layout n’a pas besoin de composant

Le layout est l’exemple le plus évident. Voici un élément ordinaire de Tailwind :

<div
  class="
    flex
    flex-col
    items-start
    gap-6
    p-6
    rounded-xl
    border
    bg-card
    shadow-sm
  "
>
  ...
</div>

Il n’y a rien de mal à cela. L’un des véritables atouts de Tailwind est que l’on peut comprendre la plupart des propriétés d’un élément sans devoir aller et venir constamment entre le modèle et le fichier de style. Mais regardez combien de fonctions remplit cet unique attribut class. Certaines classes décrivent l’identité visuelle :

rounded-xl
border
bg-card
shadow-sm

D’autres décrivent les relations spatiales entre l’élément et ses enfants :

flex
flex-col
items-start
gap-6
p-6

Le navigateur ne fait aucune distinction entre elles. class n’est rien de plus qu’un mécanisme générique permettant d’attacher des identifiants que CSS et JavaScript peuvent sélectionner, et HTML ne fait pas de distinction entre une « classe de mise en page » et une « classe de design ». Cependant, du point de vue de la conception d’une API, séparer les deux s’avère pratique. Un attribut expérimental and-layout permet d’exprimer la partie spatiale séparément :

<div
  class="card"
  and-layout="vertical align:start gap:lg p:lg"
>
  ...
</div>

Le avantage n’est pas la brièveté ; parfois cette version n’est même pas plus courte. L’avantage réside dans le fait que les responsabilités deviennent visibles : la class décrit à quoi ressemble l’élément, tandis que and-layout décrit comment il organise l’espace.

Comment l’attribut est mis en œuvre

L’implémentation n’a besoin ni de framework ni de JavaScript. Il s’agit de CSS pur : chaque élément de l’attribut est associé à des sélecteurs d’attributs (le sélecteur ~= pour les mots séparés par des espaces convient parfaitement) et mappé sur des règles Flexbox, Grid, de mise en page et adaptatives. Une déclaration comme celle-ci n’est, en fin de compte, rien d’autre que CSS Grid avec des points de rupture.

<div
  and-layout="grid cols:1 cols@md:2 cols@lg:3 gap:lg"
>

Il n’y a aucun moteur de mise en page caché derrière, aucune directive Angular, aucun composant React, et aucun conteneur de ce type à moins que vous ne souhaitiez réellement utiliser Stack :

<Stack direction="vertical" gap="lg">

Cette qualification est importante. Les composants de mise en page ne sont pas mauvais. Une abstraction <Stack> peut être très pratique, en particulier au sein d’un système de conception de framework. L’approche par attributs n’est qu’une autre option : lorsque l’élément dont vous avez besoin existe déjà, il n’est pas nécessaire d’inventer un composant supplémentaire simplement pour décrire comment ses enfants sont disposés.

Un compromis à garder à l’esprit : les attributs personnalisés sans préfixe data- ne sont pas des HTML valides selon les spécifications, même si tous les navigateurs les stylisent sans problème. Les validateurs et certains outils de détection d’erreurs s’y opposeront, et une orthographe data-and-layout permet d’éviter cela au prix d’une légère surcharge de texte.

class peut tout faire, mais ce n’est pas obligatoire

Il existe un schéma plus large derrière cela. Le marquage moderne peut reposer une grande partie des responsabilités sur class. En ajoutant du CSS utilitaire et un plugin d’animation, un élément tout à fait raisonnable peut évoluer en ceci :

<div
  class="
    card
    flex
    flex-col
    items-center
    gap-6
    p-8
    rounded-xl
    border
    bg-card
    animate-in
    fade-in
    slide-in-from-bottom-4
    duration-500
  "
>

Cela fonctionne, et des classes utilitaires lisible sont de loin préférables à l’idée que chaque interface nécessite une hiérarchie BEM parfaitement sémantique. La vraie question n’est pas de savoir si class peut gérer tout cela ; il le peut évidemment. La question est de savoir si un seul attribut devrait représenter toutes les capacités d’un élément. En séparant les responsabilités, on obtient :

<div
  class="card"
  and-layout="vertical align:center gap:lg p:xl"
  and-motion="slide-in-up"
  and-motion-duration="500ms"
>

L’élément montre désormais trois responsabilités distinctes d’un seul coup d’œil :

class       → visual identity
and-layout  → spatial organization
and-motion  → animation

Pensez-y comme à une petite application du principe de responsabilité unique au marquage. L’HTML ne l’exige pas. La motivation est que le résultat est plus facile à lire, à examiner et à modifier.

La mise en mouvement dispose de son propre canal déclaratif

Le côté animation s’appuie sur un modèle qui fonctionne bien depuis des années. Animate.css, publié à animate.style, gère entièrement les animations via des classes : il suffit d’ajouter la classe de base ainsi que le nom d’une animation pour que l’élément s’anime.

<h1 class="animate__animated animate__bounce">
  Hello
</h1>

C’est simple, familier et presque dépourvu de formalités. Les plugins d’animation orientés Tailwind suivent la même philosophie en transformant les animations en un autre ensemble d’outils composables au sein de la class.

La question intéressante ici n’est pas de savoir comment créer un système d’animation plus puissant. Cela reflète la question du layout : si l’animation constitue un domaine distinct, elle peut avoir sa propre place déclarative dans le markup. Au lieu d’ajouter des outils d’animation à côté de tout le reste :

<div
  class="
    card
    animate-in
    fade-in
    slide-in-from-bottom-4
    duration-500
  "
>

on décrit l’animation séparément :

<div
  class="card"
  and-motion="slide-in-up"
  and-motion-duration="500ms"
>

Lorsque le déclencheur est important, vous le spécifiez explicitement en même temps que le délai et la durée :

<div
  class="card"
  and-motion="slide-in-up"
  and-motion-trigger="enter"
  and-motion-duration="800ms"
  and-motion-delay="200ms"
>

Le marquage indique quelle animation s’exécute, quand elle s’exécute et comment son timing est géré ; l’implémentation s’occupe du reste. Pour un déclencheur enter, cela signifie généralement qu’un IntersectionObserver surveille l’élément. Les déclencheurs de survol et de tapage utilisent leurs propres écouteurs. Il est essentiel de noter que la requête médiatique prefers-reduced-motion peut être prise en compte en un seul endroit central, au lieu de compter sur chaque auteur de composant pour s’en souvenir.

Les attributs ne sont pas magiques. Ce qu’ils offrent, c’est un moyen pour un élément d’annoncer une capacité sans l’intégrer dans l’arborescence des composants du framework.

Déclaratif tant que cela reste utile

Cette approche a ses limites. Une animation simple d’entrée convient parfaitement à un seul attribut :

and-motion="fade-in"

La transition de sortie d’un modal représente un type différent de problème. Supposons que le modal doive s’effacer par animation et être retiré du DOM uniquement une fois l’animation terminée. Dans ce cas, le cycle de vie et l’ordre des opérations deviennent importants, et une petite instruction impérative permet d’exprimer l’intention plus clairement :

await player.play('fade-zoom-out');
closeModal();

Tenter d’encoder chaque relation liée au cycle de vie dans un attribut HTML en constante expansion rendrait l’API déclarative pire, et non meilleure. La règle à suivre est de choisir le niveau d’abstraction le plus simple qui représente fidèlement le problème, qu’il s’agisse de CSS pur, d’un attribut, d’une machine à états, d’un service de framework ordinaire ou d’un composant Web. Le style déclaratif ne doit pas devenir un dogme. Personne ne cherche à se passer de JavaScript ou des frameworks. L’objectif est d’éviter d’élever chaque problème au niveau de l’abstraction le plus complexe disponible simplement parce qu’il existe.

Les attributs en tant qu’API de petites capacités

Lorsque tant la mise en page que le mouvement suivent ce modèle, les attributs commencent à ressembler moins à des paramètres de configuration et plus à de petites API de fonctionnalités. Examinons ce fragment :

<section
  class="feature-card"
  and-layout="vertical gap:lg p:xl"
  and-motion="fade-in"
  and-motion-trigger="enter"
>
  <h2>Framework-agnostic UI</h2>
  <p>At least as much as possible.</p>
</section>

Ce n’est toujours qu’une <section>, et sa signification sémantique reste intacte. Obtenir de l’espacement, une présentation et une animation d’apparition n’a pas nécessité une série de conteneurs comme celui-ci :

<and-stack>
  <and-motion-container>
    <and-card>
      ...
    </and-card>
  </and-motion-container>
</and-stack>

Cette version imbriquée n’est pas automatiquement fausse. Si ces éléments présentent un comportement significatif et offrent une API utile, ils méritent certainement d’être des composants. La distinction est plus fine : une capacité réutilisable n’a pas nécessairement à devenir un composant supplémentaire. Souvent, le nœud DOM existe déjà et a simplement besoin d’un agencement, d’effets de mouvement ou d’un comportement de tooltip. Le framework n’a pas toujours besoin d’en être au courant, et lorsque l’abstraction est directement intégrée à l’HTML, elle peut être utilisée sans frais dans Angular, Astro, React, Vue ou même sur une page ne faisant pas appel à de framework.

Être indépendant des frameworks ne signifie pas être sans framework

Il existe ici un risque évident : l’approche « framework-agnostic » peut facilement se transformer en une autre compétition de pureté, ce qui manquerait complètement son objectif. Les frameworks gagnent leur place en intégrant divers éléments. Angular propose des signaux, des templates, une injection de dépendances, des formulaires, un routage ainsi qu’un modèle d’application clair. React dispose d’un écosystème de composition très mature. Vue et Svelte impliquent quant à eux d’autres compromis.

Le comportement indépendant des frameworks doit néanmoins être relié au système réactif de chaque application. C’est précisément pour cette raison que Zag fournit des adaptateurs de framework : un adaptateur relie une machine aux conventions de réactivité, de cycle de vie et du DOM d’un framework, et la machine peut rester indépendante uniquement parce qu’une autre couche comprend ce framework.

L’approche en couches décrite ici présente les mêmes inconvénients :

  • Un composant Angular construit sur une usine d’état générique peut nécessiter un effect() pour maintenir les entrées de signal en synchronisation avec l’état externe.
  • Un composant Web doit relier la couche d’état à ses propres appels de fonction du cycle de vie.
  • Le layout basé sur des attributs introduit un petit DSL que chaque membre de l’équipe doit apprendre.
  • Les attributs de mouvement ajoutent une autre interface API.
  • Décomposer tout le code en packages séparés crée des contrats qui doivent rester compatibles dans le temps.

Parfois, un composant simple spécifique au framework est clairement la meilleure solution en termes de conception. C’est pourquoi un ensemble de composants compatibles uniquement avec Angular reste pertinent parallèlement à tout cela. Personne ne devrait faire passer chaque bouton Angular à travers une chaîne de quatre adaptateurs, une paire de machines d’état et un composant Web simplement parce que le terme « agnostique » semble sophistiqué. C’est une façon très coûteuse d’éviter d’écrire :

<volt-button>

L’indépendance par rapport au framework s’avère utile lorsque la portabilité est une exigence réelle. Lorsqu’elle ne l’est pas, une intégration étroite au framework est souvent la meilleure solution.

Une interface utilisateur agnostique par rapport au framework en tant que stack, et non en tant que bibliothèque

L’expression « bibliothèque d’interface utilisateur agnostique aux frameworks » évoque généralement un ensemble de composants capables de fonctionner partout. Les Web Components s’en rapprochent assez pour les composants affichés. Cependant, un modèle plus utile n’est pas une bibliothèque universelle unique, mais plutôt une série de responsabilités, chacune devenant spécifique à un framework à un moment donné :

Application
                         │
          Angular / React / Vue / Astro
                         │
                  framework adapters
                         │
    ─────────────────────────────────────────
                         │
         headless behavior / state machines
                         │
     Web Components     HTML capabilities
          │                 │
          │          layout / motion attributes
          │                 │
    ─────────────────────────────────────────
                         │
                    Web Platform

Aucune application n’est obligée d’utiliser tous les niveaux :

  • Un projet utilise directement un Web Component sans jamais toucher à l’état sans interface qui se trouve en dessous.
  • Un autre n’utilise que la machine à états et construit sa propre interface Angular par-dessus.
  • Une page statique Astro peut ne nécessiter que les attributs de mise en page et de mouvement.
  • Une application Angular peut ignorer toute cette structure et utiliser un kit natif d’Angular avec des primitives Angular, car cela offre l’expérience de développement la plus fluide.

L’idée principale est justement de pouvoir combiner différents éléments. Être agnostique vis-à-vis d’un framework ne signifie pas que ce dernier est interdit ; cela veut dire que c’est vous qui décidez où se situe la frontière du framework, au lieu de le laisser s’étendre automatiquement à chaque couche réutilisable.

Réserver le framework à la couche d’application

Tout cela n’est pas une critique envers Angular ou tout autre framework. Il s’agit plutôt de déterminer quelle responsabilité un framework assume par défaut. Examinez à nouveau les exemples sous cet angle :

  • La conception visuelle d’un menu déroulant peut appartenir au produit, et son affichage à Angular, mais son modèle d’interaction n’est pas obligatoirement le même.
  • Un bouton partagé peut être un composant Web lorsque l’on souhaite avoir le même bouton dans plusieurs projets, ou un composant Angular personnalisé qui ne partage qu’un noyau comportemental minimal lorsque le produit a besoin de son propre langage visuel.
  • Une carte peut être stylisée avec Tailwind tout en conservant son layout dans and-layout.
  • Un effet d’entrée peut ne nécessiter rien de plus qu’un petit observateur et un attribut, et il ne doit pas se soucier que ce soit Astro ou Angular qui ait généré l’HTML.
  • Vus ensemble, ces éléments s’emboîtent parfaitement :

    • Headless UI élimine toute considération visuelle.
    • Machines à états libèrent le comportement de tout rendeur particulier.
    • Composants Web permettent à un composant déjà rendu de circuler entre différentes architectures.
    • Attributs dédiés ancrent des fonctionnalités légères comme le layout et les animations directement dans l’HTML lui-même, plutôt que dans le framework.

    Aucune de ces idées n’est nouvelle, et tous les systèmes d’interface utilisateur ne doivent pas être conçus de cette manière. Cependant, pris ensemble, ils remettent en question une pratique courante selon laquelle le framework doit également se trouver sous chaque abstraction d’interface utilisateur réutilisable d’une application.

    Points clés

    • Distinguez "non stylisé" de ">indépendant du rendu"; la plupart des bibliothèques headless ne correspondent qu’à la première catégorie.
    • Mettez la logique d’interaction partagée par plusieurs produits dans des machines à états ou des factories sans framework, et acceptez consciemment les coûts liés aux adaptateurs.
    • Utilisez les Web Components lorsque le composant rendu lui-même, et non seulement son comportement, doit être identique entre les différents environnements.
    • Préférez les capacités d’attributs basées uniquement sur CSS pour le layout et les mouvements simples, et passez à du code impératif dès que l’ordre des étapes de vie devient important.
  • Conservez les composants spécifiques au framework là où la portabilité n’est pas une exigence ; l’objectif est d’utiliser des frameworks sans que chaque couche ne dépende d’eux.
  • Lectures complémentaires