Aller au contenu principal
MEWA STUDIO

CSS Scroll-Driven Animations : remplacer JavaScript par du CSS natif

Publié le 27 mars 2026|13 min de lecture
CSSanimationperformance

Les CSS Scroll-Driven Animations permettent de remplacer des dizaines de lignes de JavaScript par quelques propriétés CSS. Performance, syntaxe, support navigateur, cas pratiques : le guide complet pour basculer en production.

Sorties de gouttes d'eau blanches disposées en lignes circulaires sur fond bleu foncé, évoquant le flux des CSS Scroll-Driven Animations

Vous connaissez la scène. Un designer livre une maquette avec une barre de progression liée au scroll, un reveal d'éléments au défilement et un léger parallax sur les images. Trois effets. Trois patterns que vous avez implémentés des dizaines de fois.

Vous ouvrez votre éditeur. Vous installez GSAP (47 Ko minifié), vous configurez ScrollTrigger, vous écrivez un IntersectionObserver avec ses callbacks, ses thresholds, ses unobserve. Quarante lignes de JavaScript, un bundle qui grossit, une dépendance de plus à maintenir. Et tout ça s'exécute sur le thread principal, en compétition avec le rendu, les événements utilisateur et le reste de votre logique applicative.

Maintenant, imaginez le même résultat en 5 lignes de CSS. Pas de dépendance. Pas de JavaScript. Exécution directe par le moteur de rendu du navigateur, hors du thread principal. C'est exactement ce que permettent les CSS Scroll-Driven Animations et elles sont prêtes pour la production.

Le problème avec JavaScript pour les animations de scroll

Avant de parler de la solution, posons le diagnostic. Pourquoi utilise-t-on JavaScript pour les animations liées au scroll ? Parce que, historiquement, CSS n'avait aucun moyen de relier une animation à la position de défilement. On savait animer au clic, au survol, au chargement, mais pas au scroll. JavaScript était le seul chemin.

Le problème, c'est que ce chemin a un coût technique considérable.

Le thread principal, goulot d'étranglement

Chaque événement scroll en JavaScript s'exécute sur le thread principal du navigateur. Ce même thread qui gère le parsing HTML, le calcul des styles, le layout, le painting et toute votre logique applicative. Quand vous attachez un listener à l'événement scroll, vous ajoutez du travail à chaque frame, soit potentiellement 60 à 120 fois par seconde.

Le résultat ? Du jank : ces micro-saccades visibles quand le navigateur ne parvient pas à maintenir un rendu fluide à 60 fps. Selon les données de web.dev (opens in a new tab), un scroll handler qui prend plus de 10 ms par frame provoque des chutes de performance perceptibles par l'utilisateur.

La complexité du code

Pour un simple "reveal on scroll" (faire apparaître un élément quand il entre dans le viewport), le code JavaScript standard ressemble à ceci :

javascript
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
        observer.unobserve(entry.target);
      }
    });
  },
  {
    threshold: 0.1,
    rootMargin: '0px 0px -50px 0px',
  }
);

document.querySelectorAll('.reveal').forEach((el) => {
  observer.observe(el);
});

Reveal on scroll classique avec IntersectionObserver

Plus le CSS associé :

css
.reveal {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

Styles pour le reveal JS

27 lignes pour faire apparaître un élément au scroll. Ça fonctionne, mais c'est du boilerplate pur : aucune logique métier, juste de la plomberie technique. Multipliez par le nombre d'effets sur une page (progress bar, parallax, sticky transitions) et vous obtenez un fichier d'animations de 200 lignes qui ne fait rien d'autre que dire au navigateur ce qu'il sait déjà : la position du scroll.

Le poids des dépendances

Les bibliothèques d'animation de scroll sont omniprésentes dans les projets modernes :

BibliothèqueTaille (minifié + gzip)Usage typique
GSAP + ScrollTrigger~27 KoScroll animations complexes, pinning, timelines
Framer Motion~32 KowhileInView, scroll-linked React animations
Lenis + animation lib~8 Ko + libSmooth scroll + animation au défilement
AOS (Animate On Scroll)~6 KoReveal on scroll simple

Poids des principales bibliothèques d'animation de scroll en 2026

Chaque kilo-octet supplémentaire de JavaScript impacte le Total Blocking Time (TBT), un des Core Web Vitals. Pour un site e-commerce, Google estime qu'une augmentation de 100 ms du TBT réduit les conversions de 0,3 à 0,7% (source : web.dev (opens in a new tab)).

CSS Scroll-Driven Animations : les fondamentaux

Les CSS Scroll-Driven Animations sont une spécification du W3C qui permet de lier une animation CSS à la progression du scroll plutôt qu'au temps. Au lieu de dire "cette animation dure 2 secondes", vous dites "cette animation progresse de 0% à 100% entre le début et la fin du scroll". La documentation de référence est disponible sur MDN (opens in a new tab).

Le concept repose sur deux types de timelines :

  • scroll() : lie l'animation à la progression du scroll d'un conteneur. L'animation avance quand l'utilisateur scrolle, recule quand il remonte. Idéal pour les barres de progression, les effets parallax, les en-têtes qui changent.
  • view() : lie l'animation à la visibilité d'un élément dans le viewport. L'animation se déclenche quand l'élément entre dans le champ visible. Idéal pour les reveals, les transitions d'entrée, les compteurs animés.

La propriété clé : animation-timeline

Tout repose sur une seule propriété CSS : animation-timeline. Elle remplace la timeline temporelle par défaut (auto) par une timeline liée au scroll. Le principe est le même que pour une animation classique : vous définissez des @keyframes, puis vous les rattachez, non pas à une durée, mais à une progression de scroll.

css
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.element {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

Structure de base d'une scroll-driven animation

Trois lignes de propriétés. Pas de JavaScript. Pas de dépendance. Et le navigateur gère tout, y compris l'exécution hors du thread principal.

animation-range : le contrôle fin

La propriété animation-range définit quand l'animation commence et se termine dans la timeline. Pour view(), les valeurs disponibles sont :

  • entry : du moment où l'élément commence à entrer dans le viewport jusqu'à ce qu'il soit entièrement visible
  • exit : du moment où l'élément commence à sortir jusqu'à ce qu'il soit entièrement sorti
  • contain : la période pendant laquelle l'élément est entièrement contenu dans le viewport
  • cover : la période complète, de la première apparition à la disparition totale

Vous pouvez combiner et ajuster avec des pourcentages : animation-range: entry 10% cover 50%; signifie que l'animation commence quand l'élément est entré à 10% et se termine au milieu de la phase cover.

scroll() en pratique

La fonction scroll() lie l'animation au scroll global d'un conteneur. C'est la timeline idéale pour les effets qui doivent refléter la position globale de la page.

Exemple 1 : barre de progression de lecture

L'effet le plus classique : une barre en haut de page qui indique la progression de lecture de l'article. Voici la version CSS pure :

html
<div class="progress-bar" aria-hidden="true"></div>

HTML de la barre de progression

css
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #6366f1;
  transform-origin: left;
  animation: grow-progress linear both;
  animation-timeline: scroll();
}

Barre de progression en CSS pur avec scroll()

Neuf lignes de CSS, zéro JavaScript. La barre se remplit de gauche à droite en suivant le scroll de la page. Pour obtenir le même résultat en JavaScript, il faudrait un listener sur scroll, un calcul de ratio scrollTop / (scrollHeight - clientHeight) et une mise à jour du style à chaque frame.

Exemple 2 : parallax léger sur une image

css
@keyframes parallax-shift {
  from { transform: translateY(-20px); }
  to   { transform: translateY(20px); }
}

.hero-image {
  animation: parallax-shift linear both;
  animation-timeline: scroll();
}

Parallax CSS natif avec scroll()

L'image se déplace légèrement plus lentement que le contenu, créant l'effet de profondeur classique du parallax. La valeur de translateY contrôle l'intensité de l'effet : 20px pour un parallax subtil, 80px pour un effet plus prononcé.

view() en pratique

La fonction view() est celle qui remplace le plus directement IntersectionObserver. Elle lie l'animation à la visibilité de l'élément lui-même dans le viewport.

Exemple 1 : reveal au scroll

Le reveal classique (fade-in + slide-up quand l'élément entre dans le viewport) se fait en quelques lignes :

css
@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}

Reveal on scroll en CSS pur avec view()

L'élément est invisible au départ. Dès qu'il entre dans le viewport, il glisse vers le haut avec un fondu. L'animation est complète quand l'élément a parcouru 40% de la zone d'entrée. Pas de callback, pas de classe à ajouter, pas de cleanup.

Exemple 2 : scale progressif sur une image

css
@keyframes scale-up {
  from {
    transform: scale(0.8);
    opacity: 0.5;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

.gallery-image {
  animation: scale-up linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

Zoom progressif sur une image avec view()

L'image grossit progressivement à mesure que l'utilisateur scrolle. Combiné avec un overflow: hidden sur le conteneur, cet effet donne une impression de profondeur cinématographique.

Exemple 3 : animation staggerée sur une grille

Pour un effet de stagger (décalage progressif entre les éléments d'une grille), utilisez animation-delay combiné à view() :

css
.grid-item {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 50%;
}

.grid-item:nth-child(3n + 2) {
  animation-range: entry 10% entry 60%;
}

.grid-item:nth-child(3n + 3) {
  animation-range: entry 20% entry 70%;
}

Stagger CSS natif sur une grille

Chaque colonne de la grille a un animation-range légèrement décalé. Le résultat : les éléments apparaissent en cascade, de gauche à droite, à mesure qu'ils entrent dans le viewport. Le tout sans une seule ligne de JavaScript.

Avant/après : JavaScript vs CSS natif

Comparons le même effet (reveal au scroll) dans les deux approches. L'objectif est identique : faire apparaître les éléments .card avec un fondu et un glissement vertical quand ils entrent dans le viewport.

Approche JavaScript (IntersectionObserver)

javascript
document.addEventListener('DOMContentLoaded', () => {
  const cards = document.querySelectorAll('.card');

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('is-visible');
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
  );

  cards.forEach((card) => observer.observe(card));
});

reveal.js, 22 lignes

css
.card {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s ease-out,
              transform 0.6s ease-out;
}

.card.is-visible {
  opacity: 1;
  transform: translateY(0);
}

reveal.css pour la version JS, 12 lignes

Total : 34 lignes réparties sur 2 fichiers, une dépendance au DOM chargé, un observer à instancier et nettoyer.

Approche CSS natif (Scroll-Driven Animations)

css
@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}

reveal.css en CSS pur, 14 lignes

Total : 14 lignes, un seul fichier, zéro JavaScript. Le navigateur fait tout le travail.

Approche GSAP + ScrollTrigger

Pour les projets qui utilisaient GSAP, la comparaison est encore plus parlante :

javascript
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

gsap.utils.toArray('.card').forEach((card) => {
  gsap.from(card, {
    opacity: 0,
    y: 40,
    duration: 0.6,
    ease: 'power2.out',
    scrollTrigger: {
      trigger: card,
      start: 'top 90%',
      toggleActions: 'play none none none',
    },
  });
});

Reveal GSAP avec ScrollTrigger, 18 lignes + 27 Ko de bundle

18 lignes de JavaScript, plus ~27 Ko de dépendances (GSAP core + ScrollTrigger). Le CSS natif fait la même chose en 14 lignes et 0 Ko de bundle. Pour les sites où la performance est critique (e-commerce, média, SaaS), la différence est significative.

Performance : pourquoi CSS gagne

L'argument performance ne se limite pas au poids du bundle. La différence fondamentale est architecturale : les CSS Scroll-Driven Animations s'exécutent sur le compositor thread, pas sur le main thread.

Main thread vs compositor thread

Le navigateur possède plusieurs threads. Le main thread gère JavaScript, le DOM, le calcul des styles et le layout. Le compositor thread gère la composition finale des couches, les transformations GPU et le rendu à l'écran. Quand une animation CSS utilise uniquement transform et opacity (les propriétés "compositable"), elle peut tourner entièrement sur le compositor thread, sans jamais toucher au main thread.

Selon les benchmarks de Chrome for Developers (opens in a new tab), les scroll-driven animations qui utilisent transform et opacity atteignent systématiquement 60 fps, même sur des appareils mobiles bas de gamme, là où les équivalents JavaScript chutent à 30-45 fps sous charge.

CritèreJavaScript (scroll listener)CSS Scroll-Driven
Thread d'exécutionMain thread (bloquant)Compositor thread (non-bloquant)
Framerate sous charge30 - 45 fps (variable)60 fps (constant)
Impact TBTAugmente le Total Blocking TimeAucun impact
Bundle size6 - 32 Ko (selon la lib)0 Ko
Parsing JS au chargementOui (bloque le rendu initial)Non
Garbage collectionOui (callbacks, closures)Non

Comparaison des performances entre JavaScript et CSS Scroll-Driven Animations

L'impact concret pour les utilisateurs : des animations fluides même sur un smartphone Android d'entrée de gamme, même quand la page charge du contenu dynamique en arrière-plan. Pour les propriétaires de sites, c'est un meilleur score Lighthouse, un meilleur Core Web Vitals et potentiellement un meilleur référencement.

Le support navigateur en 2026

C'est souvent l'objection principale : "Est-ce que les navigateurs supportent ça ?". En mars 2026, la réponse est oui, largement. Consultez les données à jour sur Can I Use (opens in a new tab).

NavigateurSupportDepuis
ChromeCompletVersion 115 (juillet 2023)
EdgeCompletVersion 115 (juillet 2023)
FirefoxCompletVersion 123 (février 2024)
SafariComplet (Interop 2024/2025)Version 18.4 (2025)
Chrome AndroidCompletVersion 115
Safari iOSCompletVersion 18.4

Support navigateur des CSS Scroll-Driven Animations, mars 2026

Avec Safari qui a rejoint le groupe, le support mondial dépasse 95% des utilisateurs. Pour la documentation complète, le guide officiel de WebKit (opens in a new tab) détaille l'implémentation Safari et les cas d'usage recommandés.

Progressive enhancement : la stratégie de déploiement

Même avec un support de 95%, il reste des utilisateurs sur des navigateurs anciens. La bonne pratique, c'est le progressive enhancement : offrir une expérience fonctionnelle à tous et une expérience enrichie à ceux dont le navigateur supporte la fonctionnalité.

@supports : la détection native

css
/* Base : l'élément est visible, pas d'animation */
.card {
  opacity: 1;
  transform: translateY(0);
}

/* Enhancement : si le navigateur supporte les scroll-driven animations */
@supports (animation-timeline: view()) {
  .card {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 40%;
  }
}

Progressive enhancement avec @supports

Cette approche est propre et fiable. Les navigateurs qui ne supportent pas animation-timeline ignorent le bloc @supports : l'élément reste visible, aucun contenu n'est caché. Les navigateurs modernes appliquent l'animation. Zéro risque.

Fallback avec IntersectionObserver

Pour les projets qui veulent aussi l'animation sur les anciens navigateurs, combinez CSS natif et JavaScript en fallback :

javascript
// Ne charger le fallback que si le navigateur ne supporte pas
if (!CSS.supports('animation-timeline', 'view()')) {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('is-visible');
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0.1 }
  );

  document.querySelectorAll('.card').forEach((el) => {
    observer.observe(el);
  });
}

Fallback JS conditionnel pour navigateurs anciens

Le JavaScript ne se charge et ne s'exécute que pour les 5% d'utilisateurs qui en ont besoin. Les 95% restants bénéficient de la version CSS, plus légère et plus performante.

Cas pratiques : 4 effets production-ready

Voici quatre effets que vous pouvez déployer en production dès maintenant. Chacun est testé sur les navigateurs supportés et utilise le progressive enhancement par défaut.

1. Barre de progression de lecture

Pour un blog ou un média, la barre de progression est un indicateur visuel qui améliore l'expérience de lecture. Selon Smashing Magazine (opens in a new tab), les articles avec indicateur de progression ont un taux de lecture complète supérieur de 12%.

css
@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, #6366f1, #8b5cf6);
  transform-origin: left;
  z-index: 1000;
}

@supports (animation-timeline: scroll()) {
  .reading-progress {
    animation: progress linear both;
    animation-timeline: scroll();
  }
}

Barre de progression complète, production-ready

2. Reveal staggeré sur une grille de cards

css
@keyframes card-reveal {
  from {
    opacity: 0;
    transform: translateY(30px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@supports (animation-timeline: view()) {
  .card {
    animation: card-reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 50%;
  }

  .card:nth-child(even) {
    animation-range: entry 10% entry 60%;
  }
}

Grille de cards avec reveal staggeré

3. Image qui se révèle avec un clip-path

css
@keyframes clip-reveal {
  from {
    clip-path: inset(0 100% 0 0);
    opacity: 0;
  }
  to {
    clip-path: inset(0 0 0 0);
    opacity: 1;
  }
}

@supports (animation-timeline: view()) {
  .reveal-image {
    animation: clip-reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
}

Reveal d'image avec clip-path animé

L'image se dévoile de gauche à droite à mesure qu'elle entre dans le viewport. L'effet est cinématographique et le clip-path est entièrement compositable, donc il tourne sur le GPU.

4. En-tête qui se réduit au scroll

css
@keyframes shrink-header {
  from {
    padding-block: 2rem;
    background: transparent;
  }
  to {
    padding-block: 0.5rem;
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(10px);
  }
}

@supports (animation-timeline: scroll()) {
  .site-header {
    position: sticky;
    top: 0;
    animation: shrink-header linear both;
    animation-timeline: scroll();
    animation-range: 0px 200px;
  }
}

Header compact au scroll avec scroll()

Le header passe d'un état aéré (grand padding, fond transparent) à un état compact (petit padding, fond flou) au cours des 200 premiers pixels de scroll. Attention : cette animation touche au padding, une propriété qui déclenche un layout. Elle ne pourra pas s'exécuter sur le compositor thread. Pour des performances optimales, préférez animer transform: scaleY() sur un pseudo-élément plutôt que le padding directement.

Les limites : quand garder JavaScript

Les CSS Scroll-Driven Animations ne remplacent pas tout. Elles excellent pour les animations déclaratives, liées au scroll, avec un comportement prévisible. Mais certains cas d'usage restent le territoire de JavaScript. Pour approfondir les possibilités et les limites, CSS-Tricks (opens in a new tab) propose un excellent tour d'horizon.

Chorégraphies complexes

Si vos animations doivent se déclencher en séquence avec des conditions ("jouer A, puis attendre 200 ms, puis jouer B si l'utilisateur a scrollé au-delà de la section X"), JavaScript reste nécessaire. Les timelines CSS ne supportent pas les branchements conditionnels.

Logique métier

Si l'animation dépend de données dynamiques (score de progression, état de panier, profil utilisateur), CSS ne peut pas accéder à ces informations. GSAP ou Framer Motion restent pertinents pour ces cas.

Animation 3D et WebGL

Pour les scènes Three.js ou les expériences WebGL liées au scroll (le MacBook Pro d'Apple, par exemple), JavaScript est incontournable. Les CSS Scroll-Driven Animations opèrent dans le plan 2D des propriétés CSS, pas dans un canvas WebGL.

Smooth scrolling et contrôle du défilement

Si vous utilisez Lenis ou d'autres bibliothèques pour modifier le comportement du scroll lui-même (inertie, smooth, snapping avancé), les CSS Scroll-Driven Animations ne les remplacent pas. Elles animent en fonction du scroll, mais elles ne modifient pas le scroll lui-même.

Le guide de décision

BesoinSolution recommandée
Reveal on scroll (fade, slide, scale)CSS Scroll-Driven via view()
Barre de progression de lectureCSS Scroll-Driven via scroll()
Parallax légerCSS Scroll-Driven via scroll()
Header compact au scrollCSS Scroll-Driven via scroll()
Animation 3D/WebGL liée au scrollJavaScript (Three.js + ScrollTrigger)
Chorégraphie conditionnelle complexeJavaScript (GSAP timelines)
Animation dépendante de données dynamiquesJavaScript (Framer Motion / GSAP)
Modification du comportement du scrollJavaScript (Lenis, smooth scroll)

Quand utiliser CSS Scroll-Driven vs JavaScript pour les animations

Conclusion : le basculement est en cours

Les CSS Scroll-Driven Animations ne sont pas une curiosité expérimentale. Elles sont supportées par tous les navigateurs majeurs, elles offrent des performances supérieures au JavaScript et elles réduisent la complexité du code de manière drastique.

Le schéma est familier. On l'a vu avec Flexbox (qui a remplacé les float hacks), avec CSS Grid (qui a remplacé les frameworks de grille JS), avec position: sticky (qui a remplacé des dizaines de plugins jQuery). À chaque fois, le même cycle : JavaScript comble un vide, le CSS rattrape, JavaScript perd sa raison d'être pour ce cas d'usage.

En 2026, nous sommes au point de basculement pour les animations de scroll. Les reveals, les barres de progression, les parallax légers, les transitions d'en-tête : tout cela peut (et devrait) se faire en CSS natif. Réservez JavaScript pour ce que CSS ne sait pas faire : la 3D, la logique conditionnelle, les animations dépendantes de données.

Le résultat ? Un bundle plus léger, des performances meilleures, un code plus maintenable et des utilisateurs qui ne verront jamais une saccade. C'est exactement le genre de gain technique qui se traduit directement en avantage business.