Accueil / Articles

Qui devrait gérer le comportement de l’interface utilisateur ? Choisir entre HTML, CSS et JavaScript

Un cadre pour déterminer quand les fonctionnalités natives HTML et CSS telles que dialog, popover, :has() et container queries devraient remplacer JavaScript, et quand elles ne le devraient pas.

Éditeur : REACTAPP.TOP15 min de lecture3416 mots

Sources: Not Everything Needs JavaScript Anymore

Les développeurs frontend ont recours à JavaScript par réflexe. Un menu doit s’ouvrir, alors on ajoute un état ; un clic doit être géré, donc on attache un écouteur. Une composante doit s’adapter à sa largeur, il faut donc la mesurer ; une aide contextuelle doit apparaître à côté d’un bouton, il faut alors lire les coordonnées ; un effet de défilement est nécessaire, on écoute donc les événements de défilement pour mettre à jour les styles. Les frameworks modernes facilitent tout cela : React dispose d’états et d’effets, Angular a des signaux, Vue offre des références et une réactivité, tandis que Svelte rend les mises à jour extrêmement concises. Rien de tout cela n’est faux, et JavaScript joue un rôle majeur dans le fait que le navigateur soit devenu une plateforme d’applications plutôt qu’un simple visualiseur de documents. Pourtant, HTML et CSS gagnent constamment en fonctionnalités qui chevauchent ce travail. Cet article explique pourquoi cela se produit, présente les primitives natives qui couvrent désormais les interactions courantes, et vous donne une règle pratique pour déterminer quel niveau de développement doit gérer tâche donnée.

du comportement.

Pourquoi la plateforme continue d’absorber le travail des applications

Puisque JavaScript peut déjà résoudre presque tous les problèmes d’interface utilisateur, pourquoi la plateforme s’efforce-t-elle de se développer ? Ce n’est pas parce que JavaScript a échoué. C’est parce que le navigateur apprend progressivement à gérer des problèmes que chaque application devait autrefois résoudre seule. Chaque fois que cela se produit, les développeurs disposent d’une nouvelle option, et la question devenue plus intéressante est de savoir quelle option entraîne les coûts à long terme les plus faibles.

Chaque changement concourt pour le prochain frame

Il est utile de se rappeler ce que fait le navigateur après qu’un élément sur la page a changé. Un pipeline de rendu simplifié se présente comme ceci :

JavaScript
    ↓
Style calculation
    ↓
Layout
    ↓
Paint
    ↓
Composite

Les moteurs réels sont bien plus sophistiqués. Toutes les mises à jour ne passent pas par toutes les étapes : certaines tâches s’exécutent en dehors du thread principal, la composition peut avoir lieu séparément, et certaines animations évitent de déclencher à plusieurs reprises le réajustement du layout ou la redessin. Néanmoins, une idée issue de ce modèle simplifié reste valable : le navigateur dispose d’un budget temporel limité pour générer chaque frame. L’exécution des scripts, le traitement des entrées, le calcul des styles et la rendu doivent tous partager ce temps. Plus de tâches sont ajoutées, plus il faut de temps au navigateur pour répondre. Pour en savoir davantage sur le coût de chaque type de modification, consultez notre analyse de le réajustement, la redessin et la composition.

C’est facile de passer à côté de ce problème sur une machine de développement dotée d’un CPU rapide, de beaucoup de mémoire et d’une bonne connexion. Exécutez la même application sur un téléphone plus ancien en mode économie d’énergie, avec plusieurs applications ouvertes : de petits retards deviennent alors visibles : une animation rate un frame, le défilement est lent, et un bouton répond avec un léger retard au point de donner l’impression de ne pas fonctionner.

Le travail d’optimisation des performances ne consiste pas seulement à rendre le code existant plus rapide. Souvent, la meilleure optimisation consiste à ne pas créer de tâches que le navigateur n’avait jamais besoin que vous créiez.

Il s’agit de responsabilité, pas de HTML contre JavaScript

Il serait facile de réduire cette discussion à un slogan :

HTML + CSS = good
JavaScript   = bad

Ce slogan est absurde. JavaScript permet de résoudre des problèmes pour lesquels HTML et CSS n’ont jamais été conçus. L’édition collaborative dépend de la logique de l’application, tandis que les tableaux de bord en temps réel exigent que les données restent synchronisées. Les fonctionnalités avancées telles que le glisser-déposer, les règles de domaine, les graphiques interactifs, les machines à états et les workflows en plusieurs étapes ne disparaîtront pas simplement parce qu’un nouveau sélecteur a été mis à disposition.

Les cas qui méritent d’être réexaminés sont bien plus limités. Supposons que vous ayez besoin d’afficher ou de cacher des informations supplémentaires : un script peut gérer cela.

button.addEventListener('click', () => {
  panel.hidden = !panel.hidden;
});

C’est un code tout à fait valide. Cependant, HTML peut exprimer directement un widget d’affichage. Notez que le h isolé après la balise de fermeture dans l’extrait est une faute de frappe et doit être supprimé.

<details>
  <summary>Advanced settings</summary>
  <p>...</p>
</details>h

La véritable différence ne réside pas dans le nombre de lignes. Elle concerne celui qui contrôle le comportement. Avec un script, la relation est la suivante :

Application owns the behavior

Avec <details>, c’est devenu :

Browser owns the behavior

Lorsque le navigateur gère un comportement, il gère également les détails que l’on aurait sinon dû mémoriser : le support clavier, l’état déplié exposé aux technologies d’assistance, ainsi qu’un comportement cohérent sur toutes les pages. Cette distinction est importante plus souvent qu’il n’y paraît au premier abord.

Pourquoi HTML ne s’est pas arrêté aux paragraphes et aux formulaires

En principe, HTML aurait pu cesser d’évoluer il y a longtemps. Les titres, les paragraphes, les formulaires, les boutons, les liens, les images et les conteneurs génériques suffisaient pour construire n’importe quoi, laissant tout ce qui est plus complexe aux scripts.

Pensez cependant à l’aspect de ce monde. Chaque application réinvente chaque comportement de divulgation, de dialogue, de popup et de formulaire, légèrement différemment. Les bibliothèques semblent normaliser ces schémas, les frameworks en encapsulent d’autres, et chaque utilisateur télécharge encore une implémentation de quelque chose que le navigateur pourrait comprendre nativement.

Lorsqu’un schéma apparaît partout, une primitive intégrée commence à se révéler utile. Vu de cette manière, le HTML moderne ne devient pas un framework d’application ; il encode progressivement les interactions que presque toutes les applications doivent constamment reconstruire.

Popups sans état d’application

Prenons un petit panneau de configuration. Une version de composant commence généralement par une primitive d’état, telle qu’un signal dans cet exemple ou son équivalent dans un autre framework :

const open = signal(false);

Puis les exigences s’accumulent :

button click
    ↓
change state
    ↓
show overlay
    ↓
handle outside click
    ↓
handle Escape
    ↓
manage stacking

Parfois, le produit a réellement besoin de tout cela. Pour les cas plus simples, HTML propose désormais l’API Popover. Un bouton pointe vers un élément doté de l’attribut popover via popovertarget, et le navigateur gère l’affichage et la suppression :

<button popovertarget="settings">
  Settings
</button>

<div id="settings" popover>
  ...
</div>

Avec le comportement par défaut de popover (auto), le navigateur s’occupe également de la fermeture automatique du popover lorsque l’utilisateur clique en dehors ou appuie sur Escape, et le rend dans la couche supérieure, ce qui évite de devoir gérer les niveaux de superposition z-index. Un comportement qui relevait auparavant entièrement du code de l’application dispose désormais d’un point de départ natif.

Cela ne signifie pas que vous ne devriez jamais créer un menu déroulant personnalisé. Un système de complétion automatique avec recherche à distance, virtualisation, sélection multiple et gestion personnalisée du clavier relève d’un tout autre problème. Il s’agit simplement du fait qu’un popover simple n’a plus besoin de commencer sa vie en tant qu’état de l’application.

Dialogues issus d’un élément réel

Les modaux sont une bonne illustration, car ils semblent simples tant que l’on ne les construit pas correctement. La première version se résume à un simple indicateur :

open = true;

Puis les vraies questions apparaissent :

  • Que devient le contenu derrière le modal ?
  • Où se porte l’attention lorsque celui-ci s’ouvre ?
  • Le clavier peut-il le fermer ?
  • Que se passe-t-il, et où retourne l’attention lorsqu’il se ferme ?
  • S’affiche-t-il au-dessus de tout le reste, en couche supérieure ?
  • À quoi doit ressembler l’arrière-plan et quelles fonctions doit-il avoir ?

Pendant des années, de nombreuses implémentations commençaient par un <div> générique et construisaient tout manuellement. Aujourd’hui, HTML propose un élément dédié :

<dialog>
  ...
</dialog>

Un détail pratique à connaître : le comportement modal provient de l’ouverture de l’élément avec showModal(). Cette fonction place la boîte de dialogue en premier plan, rend le reste de la page inerte, permet de la fermer avec la touche Escape et met à disposition un pseudo-élément ::backdrop pour le stylisme. L’ouverture avec show() ou l’attribut open génère quant à elle une boîte de dialogue non modale.

L’élément natif ne répond pas à toutes les exigences liées aux modaux, et les applications réelles peuvent encore y ajouter de la logique. Mais le point de départ est différent : vous n’avez plus besoin d’inventer le concept de dialogue, car le navigateur sait déjà ce qu’est un tel élément. Cela reflète un schéma récurrent dans les plateformes modernes : les primitives natives ne suppriment pas la logique de l’application ; elles réduisent simplement la quantité de logique que vous devez gérer.

CSS réactif à l’état du DOM

L’HTML n’est pas le seul niveau à assumer des tâches. Le CSS peut désormais exprimer des relations qui nécessitaient auparavant des scripts.

Prenons un champ de formulaire dont le conteneur doit être stylisé lorsque sa valeur d’entrée est invalide. Un script pourrait vérifier cette valeur et maintenir en synchronisation une classe sur le conteneur parent. Le CSS moderne permet souvent d’exprimer directement cette relation :

.field:has(input:invalid) {
  /* invalid state */
}

:has() modifie ce que le CSS peut savoir sur le document. Il était depuis longtemps demandé en tant que sélecteur parent manquant, mais les conséquences architecturales sont plus importantes que son nom : certaines synchronisations d’état n’ont plus besoin de passer par du JavaScript. Le DOM contient déjà ces informations, et le CSS peut y répondre. Il n’y a plus de classe à oublier de mettre à jour ni d’écouteur à nettoyer.

Composants qui réagissent à leur propre espace

Le design responsive s’est traditionnellement appuyé sur les requêtes médias :

@media (max-width: 700px) {
  ...
}

Cela fonctionne bien lorsque c’est la zone de visualisation qui compte. Cependant, les composants se soucient souvent d’autre chose. Imaginez la même carte affichée en deux endroits :

Main content
┌───────────────────────────────┐
│            Card               │
└───────────────────────────────┘

Sidebar
┌─────────────┐
│    Card     │
└─────────────┘

La zone de visualisation est identique dans les deux cas ; l’espace disponible pour la carte ne l’est pas. Historiquement, les systèmes de composants réutilisables résolvaient ce problème grâce à du code de mesure : on observait l’élément, on lisait sa largeur, on ajoutait une classe, et on recommençait chaque fois que le layout changeait.

Les requêtes de conteneur permettent au CSS de répondre à la vraie question, à savoir combien d’espace ce composant dispose réellement. On marque un ancêtre comme conteneur et on écrit des règles en fonction de sa taille :

.card-container {
  container-type: inline-size;
}
@container (width < 400px) {
  .card {
    /* compact layout */
  }
}

Il s’agit de plus que d’une nouvelle syntaxe responsive ; cela transfère la responsabilité. Le navigateur a déjà calculé le layout, et le CSS peut utiliser ce résultat sans que du code d’application ne le mesure à nouveau. Notre comparaison entre les requêtes de conteneur et les requêtes médias explique quand chacune convient dans un système de conception.

Le positionnement montre comment la plateforme évolue

Attacher une aide contextuelle à un bouton semble être une tâche simple jusqu’à ce que vous l’essayiez réellement. Où se trouve exactement le bouton ? De quelle taille est l’aide contextuelle ? Y a-t-il de la place en dessous, ou faut-il qu’elle apparaisse en haut ? Que se passe-t-il lors du défilement, de la modification de la taille de l’affichage, ou lorsque une autre partie du layout déplace le bouton ? Une simple aide contextuelle se transforme rapidement en problème de géométrie.

C’est pourquoi il existe des bibliothèques dédiées aux interfaces flottantes, qui résolvent très bien un problème véritablement complexe. CSS intègre désormais le positionnement par ancrage, qui permet de positionner un élément relativement à un autre élément de manière déclarative. Au moment de la rédaction, le support des navigateurs était encore inégal, il convient donc de vérifier les données de compatibilité actuelles et d’utiliser une solution de secours si vous l’adoptez.

Ce qui importe le plus, ce n’est pas que CSS remplace d’un coup toutes les bibliothèques de positionnement. Il s’agit plutôt du fait que la plateforme a reconnu le positionnement comme suffisamment courant pour mériter des primitives de niveau inférieur. C’est le cycle habituel de développement d’une plateforme : les problèmes apparaissent dans les applications, les schémas se répètent, des bibliothèques émergent, et finalement la plateforme en tire des leçons.

Animation et la valeur des API déclaratives

L’animation est le domaine où la discussion sur les performances devient plus nuancée. On dit souvent que les animations CSS s’exécutent sur la GPU tandis que celles en JavaScript se font sur le CPU. En réalité, c’est plus complexe : les animations CSS exigent quand même des efforts de traitement, JavaScript peut générer des animations très fluides, et les propriétés CSS diffèrent considérablement en termes de coût de traitement. Animer les dimensions peut déclencher un réajustement du layout, d’autres propriétés nécessitent une redessin, tandis que transform et opacity sont généralement beaucoup plus compatibles avec le composant graphique.

La distinction utile n’est donc pas entre CSS et JavaScript, mais plutôt entre l’ampleur des efforts nécessaires pour chaque modification et la liberté accordée au navigateur pour planifier ces tâches.

Les animations déclenchées par le scroll illustrent ce changement. Un effet de scroll ressemblait traditionnellement à ce cycle :

scroll event
    ↓
JavaScript calculation
    ↓
update styles
    ↓
repeat

Certaines de ces relations peuvent désormais être déclarées directement à l’engine de rendu. Au lieu de demander répétitivement au navigateur de calculer une valeur, de l’appliquer puis de le refaire, on décrit simplement que l’animation progresse le long d’une timeline de défilement. Il s’agit d’une catégorie complètement différente d’API, dans laquelle le navigateur contrôle davantage le mécanisme et peut l’optimiser. Comme pour la positionnement des ancrages, vérifiez la compatibilité dans les navigateurs ciblés.

Éliminer JavaScript, c’est enlever plus que des octets

La réduction du JavaScript est généralement évaluée en kilooctets, et cela compte, car chaque script doit passer par ce processus avant de pouvoir effectuer quoi que ce soit d’utile :

downloaded
    ↓
parsed
    ↓
compiled
    ↓
executed

Mais le cycle de vie du code d’application se poursuit longtemps après sa première exécution. Le comportement implémenté en JavaScript a tendance à entraîner :

state
event listeners
synchronization
cleanup
framework lifecycle
hydration
tests

Imaginons maintenant qu’une primitive native vous permette de supprimer cette combinaison :

state
+
outside-click listener
+
Escape handler
+
DOM synchronization

Le code supprimé ne pèse que quelques centaines de octets, mais le changement architectural est réel. Votre application ne maintient plus ce comportement. Il y a une variable d’état de moins qui peut devenir obsolète, un écouteur de moins que quelqu’un pourrait oublier de supprimer, une relation DOM de moins qui doit rester synchronisée avec l’état, et une interaction de moins à reconstruire depuis zéro, souvent avec une accessibilité inférieure à celle de la version native. Souvent, le plus grand avantage d’une fonctionnalité de plateforme n’est pas la vitesse, mais la responsabilité que vous restituez au navigateur.

La plateforme n’a pas de version finale

Une idée fausse persistante explique pourquoi les développeurs manquent ces changements : parler de « HTML5 » comme si HTML avait atteint la version cinq puis s’était principalement arrêté là. Ce n’est pas le cas. Il n’y a pas de HTML6 en développement, ni de HTML7 à attendre. HTML est maintenu en tant que standard vivant, et la spécification évolue continuellement.

CSS suit un modèle similaire. De nombreux développeurs ont appris « CSS3 », mais CSS n’est plus fourni sous forme d’un seul grand paquet versionné. Des modules distincts évoluent à leur propre rythme, c’est pourquoi on trouve de tels noms sans qu’il existe de « CSS6 » global :

Selectors Level 4
CSS Color Level 5
CSS Cascade Level

La dernière entrée est tronquée dans l’extrait ; le module de cascade et d’héritage possède également son propre numéro de niveau. Comme les modules sont indépendants, la grille peut évoluer sans attendre les sélecteurs, le travail sur les couleurs se fait séparément des polices, et les requêtes de conteneur ont été intégrées sans attendre un jalon imaginaire du CSS4. À l’échelle du web, une évolution progressive est la seule approche sensée.

JavaScript évolue également

Cela vaut aussi pour le langage. La spécification normalisée est ECMAScript ; JavaScript est le nom que les développeurs donnent au langage que les moteurs implémentent conformément à cette spécification. C’est pourquoi les progrès sont décrits en termes d’éditions ECMAScript plutôt que de versions numérotées de « JavaScript », et de nouvelles fonctionnalités continuent d’apparaître.

Ainsi, la plateforme n’est pas figée grâce à cette combinaison :

HTML5
+
CSS3
+
JavaScript

Tous les trois niveaux continuent d’évoluer. ECMAScript intègre de meilleures primitives, CSS gère davantage de fonctionnalités liées à la mise en page, au stylisme et aux interactions, tandis qu’HTML acquiert des sémantiques et des comportements que les applications devaient autrefois implémenter elles-mêmes. La frontière entre eux évolue avec le temps, c’est pourquoi ce que vous avez appris sur cette plateforme il y a des années nécessite une mise à jour régulière.

Les connaissances sur la plateforme dépassent le choix des frameworks

Ces fonctionnalités existent indépendamment de tout framework. Une requête de conteneur fonctionne de la même manière que le composant ait été créé avec Angular, React, Vue, Svelte ou du code DOM pur. :has() ne se soucie pas de la manière dont le DOM a été généré. Un <dialog> natif reste un dialogue au sein d’un framework, et un popover est une fonctionnalité du navigateur quel que soit le moyen qui l’a affiché.

Cela rend les connaissances relatives aux plateformes exceptionnellement durables. L’expertise dans un framework se transfère aux projets qui utilisent ce dernier ; l’expertise en matière de plateformes se transfère à presque tous les projets web. Cela ne diminue en rien l’importance des frameworks, qui résolvent de gros problèmes d’architecture et d’expérience utilisateur. Comprendre la couche sous-jacente vous offre une option supplémentaire lorsque l’abstraction n’est pas nécessaire, et, tout aussi important, vous montre ce que l’abstraction fait pour vous. Si vous souhaitez un catalogue de remplacements spécifiques, notre aperçu des fonctionnalités natives d’HTML qui remplacent les bibliothèques UI courantes couvre chaque fonctionnalité en détail.

Lorsque l’approche native n’est pas la bonne option

  • une recherche asynchrone ;
  • des centaines d’options virtualisées ;
  • une sélection multiple ;
  • une navigation avancée au clavier ;
  • des règles de filtrage personnalisées ;
  • des options chargées depuis le serveur ;
  • des sélections synchronisées avec l’état de l’application.

L’existence d’un simple popover ne résout en rien ce problème, et tordre le design pour éviter JavaScript rendrait l’architecture pire plutôt que meilleure.

CSS mérite le même réalisme. Il effectue un travail réel : le mise en page et la rendu ne sont pas gratuits, un DOM volumineux reste coûteux, et des animations mal choisies peuvent encore donner de mauvais résultats.

Ainsi, l’objectif n’est pas d’utiliser HTML et CSS chaque fois que c’est techniquement possible. Une règle bien plus utile est la suivante :

Utilisez le niveau le plus bas qui résout proprement la exigence réelle.

Parfois il s’agit d’HTML, parfois de CSS, parfois de JavaScript, et la plupart des bonnes applications combinent les trois. Une façon rapide d’appliquer cette règle consiste à se demander, dans l’ordre : un élément ou attribut sémantique exprime-t-il ce comportement ? Sinon, le CSS peut-il le déduire à partir de l’état déjà présent dans le DOM ? Ce n’est que lorsque les deux réponses sont négatives, ou lorsque l’exigence dépasse ce que la version native prend en charge, que le comportement doit être implémenté dans du code d’application.

Les cas les plus simples méritent les solutions les plus simples

Les fonctionnalités complexes continueront d’exiger JavaScript, et ce n’est pas un problème. Mais autour de ces fonctionnalités, la plupart des applications comportent des centaines d’interactions mineures : sections extensibles, superpositions légères, cartes qui s’adaptent à leur emplacement, mise en forme des champs invalides, transitions simples, un élément qui suit un autre. Chaque fois qu’un élément natif, un attribut ou une règle CSS courte couvre l’une d’entre elles, votre application n’a peut-être pas du tout besoin de la gérer elle-même.

Le fait que JavaScript puisse résoudre ce problème n’est pas surprenant ; il peut résoudre presque tout. Ce qui est précieux, c’est d’avoir une autre option.

Mettre à jour le modèle mental

De nombreux développeurs ont appris le web grâce à une division claire du travail :

HTML → structure
CSS → presentation
JavaScript → behavior

C’est un bon modèle pour apprendre, mais les navigateurs d’aujourd’hui sont bien plus performants que ce qu’il suggère. L’HTML intègre désormais du comportement, le CSS réagit aux relations complexes dans le DOM et à la mise en page, et le langage lui-même continue d’acquérir de nouvelles fonctionnalités. La plateforme évolue constamment, même lorsque les habitudes restent inchangées.

Beaucoup de ces habitudes étaient considérées comme des vérités indiscutables : les superpositions nécessitent un état d’application, le positionnement requiert des scripts, les composants ne peuvent réagir qu’aux points de rupture de la vue, et les animations de défilement nécessitent des écouteurs d’événements. Tout cela était peut-être vrai autrefois, mais ce n’est plus toujours le cas aujourd’hui.

C’est pourquoi rester à jour en matière d’HTML et de CSS ne consiste pas à accumuler des syntaxes ingénieuses. Une nouvelle capacité du navigateur peut éliminer une dépendance, un écouteur d’événement, un élément d’état, un ResizeObserver, ou tout code dont la seule fonction était de reproduire quelque chose que le navigateur comprend désormais nativement.

Points clés

  • Le navigateur continue d’adopter des comportements que les applications réimplémentent à plusieurs reprises ; chaque adoption vous offre une option moins coûteuse.
  • Le temps d’exécution des frames constitue un budget partagé, donc éviter les tâches inutiles est souvent l’optimisation la plus efficace.
  • Pensez en termes de responsabilité : <details>, l’API Popover ainsi que <dialog> avec showModal() permettent au navigateur de gérer les comportements, y compris les aspects d’accessibilité que vous devriez normalement assurer vous-même.
  • :has() et les requêtes de conteneur permettent au CSS de réagir à l’état du DOM et à la taille des composants, sans nécessiter de code de synchronisation ou de mesure.
  • La positionnement des liens et les animations déclenchées par le défilement vont dans la même direction, mais vérifiez le niveau de support actuel des navigateurs avant de vous en fier à eux.
  • Le natif n’est pas automatiquement meilleur : les widgets complexes restent à implémenter en JavaScript, et le travail lié au CSS comporte également des coûts réels.
  • Au moment d’ajouter une nouvelle variable d’état, un hook, un signal, une directive, un écouteur ou une dépendance, demandez d’abord si la plateforme peut déjà le faire, et utilisez le niveau le plus bas qui répond proprement à ce besoin.