CSS Scroll-Driven Animations : remplacer JavaScript par du CSS natif
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.

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 :
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é :
.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èque | Taille (minifié + gzip) | Usage typique |
|---|---|---|
| GSAP + ScrollTrigger | ~27 Ko | Scroll animations complexes, pinning, timelines |
| Framer Motion | ~32 Ko | whileInView, scroll-linked React animations |
| Lenis + animation lib | ~8 Ko + lib | Smooth scroll + animation au défilement |
| AOS (Animate On Scroll) | ~6 Ko | Reveal 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.
@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 visibleexit: du moment où l'élément commence à sortir jusqu'à ce qu'il soit entièrement sorticontain: la période pendant laquelle l'élément est entièrement contenu dans le viewportcover: 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 :
<div class="progress-bar" aria-hidden="true"></div>HTML de la barre de progression
@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
@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 :
@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
@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() :
.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)
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
.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)
@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 :
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ère | JavaScript (scroll listener) | CSS Scroll-Driven |
|---|---|---|
| Thread d'exécution | Main thread (bloquant) | Compositor thread (non-bloquant) |
| Framerate sous charge | 30 - 45 fps (variable) | 60 fps (constant) |
| Impact TBT | Augmente le Total Blocking Time | Aucun impact |
| Bundle size | 6 - 32 Ko (selon la lib) | 0 Ko |
| Parsing JS au chargement | Oui (bloque le rendu initial) | Non |
| Garbage collection | Oui (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).
| Navigateur | Support | Depuis |
|---|---|---|
| Chrome | Complet | Version 115 (juillet 2023) |
| Edge | Complet | Version 115 (juillet 2023) |
| Firefox | Complet | Version 123 (février 2024) |
| Safari | Complet (Interop 2024/2025) | Version 18.4 (2025) |
| Chrome Android | Complet | Version 115 |
| Safari iOS | Complet | Version 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
/* 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 :
// 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%.
@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
@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
@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
@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
| Besoin | Solution recommandée |
|---|---|
| Reveal on scroll (fade, slide, scale) | CSS Scroll-Driven via view() |
| Barre de progression de lecture | CSS Scroll-Driven via scroll() |
| Parallax léger | CSS Scroll-Driven via scroll() |
| Header compact au scroll | CSS Scroll-Driven via scroll() |
| Animation 3D/WebGL liée au scroll | JavaScript (Three.js + ScrollTrigger) |
| Chorégraphie conditionnelle complexe | JavaScript (GSAP timelines) |
| Animation dépendante de données dynamiques | JavaScript (Framer Motion / GSAP) |
| Modification du comportement du scroll | JavaScript (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.