CSS moderne : remplacer JavaScript pour interactions UI et performance

Remplacez le JS inutile par CSS natif (details, container queries, :has(), popover, @layer) pour des UI plus rapides, accessibles et stables sur PrestaShop.

Écran d'ordinateur affichant du code JavaScript et des graphiques de performance.

Table des matières :

  1. CSS moderne : définition opérationnelle et périmètre “remplacer JavaScript”
  2. Interactions UI “CSS-first” réellement robustes (sans hacks fragiles)
  3. Modales et dialogues : le cas où “zéro JS” est souvent un mensonge
  4. Layout et responsive : CSS moderne = moins de JS de “mesure” et moins de reflow
  5. Sélecteurs, cascade et dette CSS : :has(), @layer, tokens et compatibilité
  6. Perf : pourquoi déplacer des interactions de JS vers CSS améliore INP/TBT (mesurable)
  7. Intégrer CSS-first proprement dans PrestaShop 8/9 (build, overrides, risques)
  8. Accessibilité, préférences utilisateur et garde-fous (sans retomber dans le JS “pansement”)

CSS moderne : définition opérationnelle et périmètre “remplacer JavaScript”

Quand on parle de CSS moderne en 2026, on ne parle pas de “jolies animations” mais d’un ensemble de briques capables de gérer de l’état, du layout adaptatif et des micro-interactions en s’appuyant sur des mécanismes natifs (HTML/CSS + moteur de rendu) plutôt que sur du JavaScript “de plomberie” qui ne fait que basculer des classes. Concrètement : sélecteurs niveau 4 (:has(), :is(), :where()), container queries (@container), cascade layers (@layer), variables CSS (--tokens) et media queries orientées accessibilité (prefers-reduced-motion, prefers-color-scheme, parfois prefers-contrast). Dans un thème PrestaShop (8.x ou 9.x), ça se traduit par des interactions UI (accordéon, menu, onglets simples, popover) qui basculent d’état via HTML natif et CSS plutôt que via jQuery/Bootstrap.

“Remplacer JavaScript” veut dire, de façon opérationnelle : supprimer du JS là où il ne sert qu’à faire du DOM state toggling, et où le navigateur a déjà un mécanisme robuste (état, focus, clavier, fermeture au clic extérieur, etc.). Ce n’est pas une croisade anti-JS : sur PrestaShop, vous aurez toujours du JS pour les cas réellement dynamiques (AJAX panier, variantes produit, tracking, A/B testing, recherche instantanée, personnalisation, etc.). Le but est plutôt d’éviter le pattern habituel : un bundle qui parse/compile pour cliquer sur un bouton “+”.

Pour clarifier le périmètre, voici une grille simple (utile en audit de thème) :

Besoin UI Option HTML/CSS “native” JS requis ? Commentaire e-commerce
FAQ / accordéon <details><summary> Non Bon support, clavier OK, faible risque
Sous-menu simple :focus-within + structure HTML propre Non (souvent) Mobile = privilégier un bouton/<details>
Tooltip / mini-panneau Popover API (popover) Non Très bon pour “aide taille”, “infos livraison”
Modal bloquante <dialog> Oui (minimal) L’ouverture modale passe par showModal()
Tabs complexes (ARIA) (CSS-only possible) Souvent oui Pour un vrai pattern accessible, JS reste pertinent

Cette approche a un impact direct sur la perf et la stabilité : moins de JS = moins de download, moins de parse/compile, moins de long tasks, et donc moins de risques de dégradation des Core Web Vitals (notamment INP).

Interactions UI “CSS-first” réellement robustes (sans hacks fragiles)

Le premier pattern à basculer en CSS-first, c’est l’accordéon via <details><summary>. Ici, le navigateur gère l’état (ouvert/fermé), le focus et le clavier. CSS ne fait que styliser l’état avec details[open].

<details class="faq">
  <summary>Délais de livraison</summary>
  <div class="faq__content">…</div>
</details>
.faq { border: 1px solid #e5e7eb; border-radius: 8px; padding: .75rem; }
.faq > summary { cursor: pointer; font-weight: 600; }
.faq[open] { border-color: #111827; }
.faq__content { margin-top: .5rem; }

Améliorations “production” qui évitent les surprises en boutique :

  • Zone cliquable : gardez un <summary> explicite (pas une icône seule), sinon vous dégradez l’utilisabilité mobile.
  • Indicateur d’état : vous pouvez ajouter une flèche en pseudo-élément (sans JS) et la faire pivoter sur [open].
  • Animation : évitez les hacks de hauteur si vous n’en avez pas besoin (c’est souvent là que le CSS devient fragile). Si vous animez, prévoyez prefers-reduced-motion (plus bas).

Deuxième pattern : menus et sous-menus basés sur :focus-within et :hover, pour éviter un JS de “toggle”. Typiquement sur un header PrestaShop, vous pouvez ouvrir un sous-menu quand le focus est dans le conteneur.

  • Avantage : l’accessibilité clavier peut être correcte si la structure HTML est cohérente (liens focusables, ordre de tabulation).
  • Limite : sur mobile, hover est ambigu. En pratique, un pattern robuste consiste à utiliser <details> pour les entrées qui ont des sous-niveaux, ou un bouton + Popover API pour un mini-panneau.

Troisième pattern : popover / infobulle / mini-panneau. L’HTML Popover API (attribut popover + popovertarget) permet d’ouvrir/fermer sans JS et se style via :popover-open. C’est un bon remplacement aux micro-tooltips JS qui polluent les pages produit (et ajoutent souvent des listeners globaux).

Exemple minimal (utile sur une fiche produit : “Infos retours”, “Guide des tailles”, “Détails livraison”) :

<button type="button" popovertarget="shipping-info">
  Infos livraison
</button>

<div id="shipping-info" popover class="popover">
  <p><strong>Livraison</strong> : expédition sous 24/48h ouvrées (selon stock).</p>
  <p>Retours : 14 jours, conditions sur la page dédiée.</p>
</div>
.popover {
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: .75rem;
  width: min(360px, 92vw);
  box-shadow: 0 12px 30px rgb(0 0 0 / .12);
}
.popover:popover-open { outline: none; }

Support et nuances évoluent par navigateur : vérifiez votre matrice avant de supprimer le JS historique, et prévoyez un fallback acceptable (par exemple : un lien vers une section “Livraison & retours” plus bas, ou un <details>).

  • Référence (spéc) : HTML Popover API (WHATWG) et documentation MDN sur l’attribut popover.

Modales et dialogues : le cas où “zéro JS” est souvent un mensonge

Pour les modales, le standard viable est <dialog>. Il gère le focus, ::backdrop et une bonne partie du comportement attendu. Mais l’ouverture “correcte” en modal (showModal()) reste une API JS ; sans JS, vous ne déclenchez pas un vrai modal dialog (au mieux, vous simulez un panneau). C’est un bon exemple de compromis : CSS pour le rendu, JS minimal pour l’activation.

<button type="button" data-open-dialog="newsletter">S’abonner</button>
<dialog id="newsletter" class="dlg" aria-labelledby="dlg-title">
  <form method="dialog">
    <h2 id="dlg-title">Newsletter</h2>
    <p>…</p>
    <button value="close">Fermer</button>
  </form>
</dialog>
.dlg { border: 0; border-radius: 12px; padding: 1rem; width: min(560px, 92vw); }
.dlg::backdrop { background: rgb(0 0 0 / .55); }

Le JS associé tient en quelques lignes (event delegation), sans dépendance, sans framework :

document.addEventListener('click', (e) => {
  const btn = e.target.closest('[data-open-dialog]');
  if (!btn) return;
  const id = btn.getAttribute('data-open-dialog');
  const dlg = document.getElementById(id);
  if (dlg && typeof dlg.showModal === 'function') dlg.showModal();
});

Le gain n’est pas “pas de JS”, le gain est : pas de librairie (Bootstrap/jQuery) pour juste ouvrir/fermer.

Pour des modales “SEO” type “lire plus” sur fiches produit, un fallback CSS-only via :target reste possible, mais ça pollue l’URL (#modal) et peut interagir avec l’historique, ce qui est rarement acceptable en checkout. En e-commerce, on évite les hacks qui cassent le back/forward, surtout sur mobile.

Enfin, attention au couplage PrestaShop : certains modules injectent déjà des handlers sur clicks (ex : quickview, panier, cookies). En remplaçant une modale JS existante, testez :

  • collisions d’event listeners (double ouverture, fermeture instantanée),
  • ordre de z-index (dialog/backdrop vs sticky header),
  • scroll du body (éviter les scroll locks concurrents).

Utilisez les DevTools pour tracer les listeners et le coût des scripts (voir : DevTools Google : assistant IA pour déboguer et optimiser HTML/CSS/JS).

Layout et responsive : CSS moderne = moins de JS de “mesure” et moins de reflow

Le gros levier CSS moderne, côté UI, ce n’est pas l’animation : c’est la suppression du JS de mesure (calculs de largeur/hauteur, repositionnement au resize, égalisation de colonnes). Les container queries (@container) remplacent une partie du JS “si la card est dans une sidebar alors…” : le composant devient réellement contextuel.

.card { container-type: inline-size; }
@container (min-width: 520px) {
  .card { display: grid; grid-template-columns: 160px 1fr; gap: 1rem; }
}

Dans PrestaShop, c’est très utile pour les blocs réutilisés (miniature produit, cross-selling, bloc panier) qui se retrouvent dans des zones de largeur variable selon le thème et les modules. Au lieu de recoder trois variantes bootstrap (col-*) et un JS de réarrangement, vous laissez le navigateur choisir la mise en page en fonction du conteneur réel.

Autres briques CSS qui remplacent souvent du JS “de bricolage” :

  • aspect-ratio : plus besoin de calculer une hauteur d’image en JS pour éviter le CLS.
  • clamp() : typographie fluide sans listener resize.
  • Grid/Flex pour “equal height” : beaucoup de scripts d’égalisation disparaissent si vos cartes sont en grid avec des zones qui s’étirent naturellement.
  • content-visibility (au cas par cas) : peut réduire le coût de rendu de longues pages listing, mais à tester car cela modifie le comportement de certaines métriques et de l’ancrage (ne pas activer sans mesure).

Autre brique souvent sous-estimée : position: sticky, scroll-snap, scroll-margin-top, overscroll-behavior. Sur une page produit, un sticky “Add to cart” ou un sommaire ancré peut être fait en CSS sans JS de scroll listener (qui génère souvent des long tasks). Le scroll listener est un classique qui dégrade l’INP dès que le thread principal est chargé (tags, modules, scripts marketing).

Checklist rapide (audit) : si vous voyez du JS qui fait une de ces actions, il y a souvent une alternative CSS moderne :

  • lire offsetWidth/offsetHeight en boucle au resize,
  • recalculer des positions au scroll,
  • forcer des hauteurs identiques sur des cartes,
  • appliquer des classes “mobile/desktop” alors qu’une media query suffit.

Sélecteurs, cascade et dette CSS : :has(), @layer, tokens et compatibilité

:has() est le sélecteur qui change la donne (parent selector) : styliser un conteneur en fonction d’un descendant (ex : “si une checkbox est cochée, ouvrir un panneau”). Ça supprime une partie du JS de “toggle class sur le parent”. Exemple typique : un drawer de filtres où l’état est porté par un input.

<label class="filters">
  <input class="filters__toggle" type="checkbox" />
  <span>Filtres</span>
  <div class="filters__panel">…</div>
</label>
.filters__panel { display: none; }
.filters:has(.filters__toggle:checked) .filters__panel { display: block; }

Techniquement, c’est propre : pas de JS, pas de mutation DOM. Mais il faut être lucide : le support de :has() est aujourd’hui bon sur les navigateurs modernes, mais pas universel si votre audience inclut des environnements anciens. La règle pratique : progressive enhancement.

Exemple de garde-fou simple :

/* Fallback : panneau visible si :has n'est pas supporté */
.filters__panel { display: block; }

@supports selector(:has(*)) {
  .filters__panel { display: none; }
  .filters:has(.filters__toggle:checked) .filters__panel { display: block; }
}

Deux sélecteurs utiles pour réduire la dette CSS dans des thèmes PrestaShop “chargés” :

  • :where() pour éviter la sur-spécificité (spécificité = 0), pratique quand vous voulez des styles faciles à surcharger.
  • :is() pour factoriser sans exploser la cascade (spécificité = celle du sélecteur le plus spécifique dans la liste).

@layer est l’autre pièce indispensable si vous voulez éviter la guerre de spécificité entre le thème, Bootstrap et les modules. Vous pouvez isoler vos surcharges dans une couche “theme-overrides” qui passe après les vendor styles, sans escalader à coups de !important.

Exemple (idée de structure) :

@layer reset, vendor, modules, theme, theme-overrides;

@layer vendor {
  /* Bootstrap / CSS de librairie */
}

@layer theme-overrides {
  /* Vos corrections ciblées PrestaShop + modules */
}

Sur une boutique où plusieurs modules injectent des styles, @layer apporte un bénéfice très concret : vous stabilisez l’ordre de priorité sans augmenter la spécificité (donc sans créer une dette ingérable à la prochaine refonte).

Perf : pourquoi déplacer des interactions de JS vers CSS améliore INP/TBT (mesurable)

Le coût principal du JS n’est pas “l’exécution de votre handler click” ; c’est tout ce qui vient avant : téléchargement, décompression, parse, compilation, exécution, plus les effets secondaires (polyfills, listeners globaux, initialisation de plugins). Ces coûts sont détaillés dans la littérature perf côté Chrome/V8, et expliquent pourquoi “un petit script” peut devenir un gros problème sur mobile milieu de gamme.

En 2026, la métrique à surveiller en prod côté UX est INP (Interaction to Next Paint), devenue Core Web Vital. Un toggle de menu en JS, si votre thread est occupé (analytics, tag manager, scripts de modules), peut retarder la prochaine frame ; la même interaction gérée par l’HTML/CSS natif (<details>, popover) est souvent plus résiliente car elle réduit le travail JavaScript au moment critique de l’interaction.

Pour garder une approche pragmatique, raisonnez “pipeline” :

  • CSS/HTML natif : le navigateur bascule un état connu (open/close), met à jour le rendu, et vous ne payez pas le coût d’initialisation d’une librairie.
  • JS + plugin : vous payez l’initialisation (souvent au DOMContentLoaded), l’attachement de listeners, parfois des recalculs et du layout thrashing si le plugin mesure/positionne.

Mini-table de lecture (utile en restitution d’audit) :

Symptôme mesuré (DevTools/RUM) Cause fréquente Ce que CSS-first peut enlever
INP dégradé sur clic menu/filtre main thread occupé init plugin + toggles + recalculs
Long tasks au chargement parse/compile bundles dépendances UI non essentielles
Layout shifts en listing JS mesure images/cartes aspect-ratio, grid/flex
Jank au scroll scroll listeners position: sticky, ancres CSS

Côté PrestaShop, le gain est très concret sur les pages à forte densité de modules (home, listing, fiche produit). Réduire les dépendances front et les scripts inutiles complète les optimisations back/cache. Pour la partie “stack”, ça s’inscrit dans la même logique que les optimisations de cache décrites ici : optimiser la performance avec cache Smarty, CCC et CDN. Si un lien 404 ressort : corrigez l’URL dans votre CMS, ne masquez pas le problème.

Pour mesurer correctement, ne restez pas sur Lighthouse “one shot”. Méthodo simple, reproductible :

  1. Inventoriez les scripts UI (menu, accordéons, tooltips, sliders, tabs).
  2. Mesurez : dans Chrome DevTools > Performance, enregistrez une interaction (ouvrir un menu, filtrer un listing). Repérez les tâches > 50 ms et la part “Scripting”.
  3. Remplacez une interaction (ex : accordéon) par un pattern natif.
  4. Re-mesurez le même scénario et comparez (avant/après) : CPU, long tasks, délai visuel, et idéalement INP via RUM.

Pour INP : la doc web.dev (Core Web Vitals / INP) est un bon point d’entrée et rappelle les principes de mesure terrain vs lab.

Intégrer CSS-first proprement dans PrestaShop 8/9 (build, overrides, risques)

Contexte versions : PrestaShop 8.x et 9.x n’imposent pas une stack front unique. Dans la pratique, beaucoup de thèmes héritent d’un historique Bootstrap/jQuery. Passer en CSS-first demande surtout de maîtriser où et comment vos assets sont livrés : compilation SASS/POSTCSS, minification, et ordre de chargement.

Plan de migration réaliste (qui limite les régressions) :

  • Étape 1 — Cartographie : listez toutes les interactions UI “simples” (toggle, accordéon, tooltip, sous-menu). Identifiez ce qui est “métier” (AJAX panier) vs “cosmétique”.
  • Étape 2 — Substitution composant par composant : remplacez d’abord les patterns à faible risque : <details>, sticky, aspect-ratio.
  • Étape 3 — Dépendances : seulement après substitution, supprimez (ou conditionnez) les scripts/librairies devenus inutiles.
  • Étape 4 — Compatibilité : validez sur les navigateurs réels de votre audience (analytics), et pas uniquement sur votre poste.
  • Étape 5 — Mesure prod : vérifiez INP/LCP/CLS avant/après, car un “gain théorique” peut être annulé par un module tiers.

Au niveau technique, vous voulez : (a) un CSS critique minimal (header, above-the-fold), (b) le reste en CSS non-bloquant quand c’est possible, (c) un ordre de cascade stable (idéalement via @layer). Attention au mode CCC (Combine, Compress and Cache) : il peut réordonner/concaténer de manière surprenante selon le thème. Testez en préprod à chaque changement, idéalement sur un environnement qui colle à la prod (voir : environnement de staging Docker fidèle à la production et aussi démos PrestaShop générées à la demande via CI/CD).

Sur les modules, soyez strict : si un module charge un JS juste pour un accordéon, vous avez deux options propres :

  1. surcharger le template pour utiliser <details>,
  2. garder le HTML module mais remplacer l’interaction par CSS/HTML natif (ex : popover).

Dans les deux cas, vérifiez la compatibilité navigateur de votre audience. Ne faites pas de “purge” agressive sans métriques : en e-commerce, un bug UI coûte plus cher qu’un gain de 20 kB.

Enfin, n’oubliez pas l’angle SEO technique : moins de JS réduit les risques de rendu différé et de “page vide” perçue pendant le chargement, ce qui rejoint des problématiques d’audit type contenu non rendu/HTML incomplet. À relier avec : checklist SEO technique PrestaShop (indexation + performance) et diagnostiquer une page web vide côté SEO technique.

Accessibilité, préférences utilisateur et garde-fous (sans retomber dans le JS “pansement”)

Le CSS-first ne vous dispense pas de l’accessibilité ; au contraire, il la rend plus visible parce que vous ne pouvez plus “corriger” au runtime. Trois garde-fous simples : (1) utilisez :focus-visible pour des focus rings propres, (2) respectez prefers-reduced-motion pour neutraliser les animations, (3) évitez les interactions qui reposent sur hover seul. Sur un checkout, une animation mal pensée peut être perçue comme une latence.

Exemple :

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

Ce genre de règle est volontairement radicale : elle force votre UI à rester utilisable même sans motion. Ensuite vous affinez composant par composant. Dans PrestaShop, c’est pertinent sur des thèmes “très animés” qui génèrent de la jank sur mobile.

Ajoutez un garde-fou souvent oublié : l’accessibilité n’est pas qu’un sujet UX, c’est aussi un sujet de conformité. En Europe, la directive (UE) 2019/882 (European Accessibility Act) encadre des exigences d’accessibilité pour certains produits et services (dont des services numériques, selon les cas et transpositions nationales). Sans entrer dans le juridique, le point pratique pour un front PrestaShop : un pattern natif (<details>, <dialog>, popover) vous donne souvent une base clavier/focus plus saine qu’un composant JS “maison” mal implémenté.

Côté sécurité/robustesse, réduire le JS est aussi une façon indirecte de réduire votre surface d’attaque (moins de dépendances, moins de supply chain). Ça ne remplace pas un durcissement serveur, mais ça va dans la même direction que les pratiques SBOM/WAF/CI/CD évoquées dans : sécuriser APIs et modules PrestaShop avec SBOM, WAF et CI/CD. Là encore : pas de posture, juste du risk management.


Références externes utiles (docs)


À lire aussi