Ajouter

Lorem ipsum

Lorem ipsum

Design

5 min

Animations au scroll sans GSAP : la technique CSS + JS minimaliste

Comment faire apparaître vos sections au scroll sans librairie JS : une technique 100% CSS avec 15 lignes de JavaScript, pensée pour la performance et la conversion.

GSAP, ScrollTrigger, AOS, Locomotive Scroll... Sur la plupart des briefs Webflow, dès qu'on parle d'animation au scroll, le réflexe c'est d'aller chercher une librairie. Ça se comprend : ces outils sont puissants, bien documentés, et permettent de faire à peu près n'importe quoi.

Le problème, c'est que "n'importe quoi" n'est pas toujours le bon objectif. Sur un site Awwwards, où l'animation EST le produit, on peut se permettre d'ajouter 60 à 100 Ko de JS pour des séquences complexes. Sur un site de conversion (landing page SaaS, site vitrine B2B, page produit), chaque kilo-octet compte : le poids ralentit le chargement, et une animation trop présente distrait l'utilisateur de l'action qu'on veut qu'il fasse.

Dans ce cas, la bonne question n'est pas "quelle lib choisir" mais "est-ce qu'on a vraiment besoin d'une lib". Pour un cas d'usage simple comme faire apparaître une section en fondu quand elle entre dans l'écran, la réponse est souvent non. Voici la technique qu'on utilise en interne : quasi gratuite en performance, fiable sur tous les devices, et qui tient en une quinzaine de lignes de JS.

Le principe : le JS ouvre la porte, le CSS gère tout le reste

L'idée tient en une phrase : le JavaScript ne fait qu'ajouter ou retirer une classe CSS quand un élément entre dans la zone visible de l'écran (le viewport). Toute la partie visuelle, l'apparition, la mise à l'échelle, la vitesse de transition, est gérée en CSS pur.

Concrètement :

  • Au chargement et à chaque scroll, on calcule la position de chaque section par rapport à l'écran avec getBoundingClientRect().
  • Si la section est entrée dans le viewport (son bord haut a dépassé un certain seuil), on lui ajoute la classe is--in-viewport.
  • Le CSS définit l'état "invisible" par défaut (opacity: 0.5, scale: 0.5) et l'état "visible" quand la classe est présente (opacity: 1, scale: 1), avec une transition en cubic-bezier qui gère l'animation entre les deux.

Zéro dépendance, zéro fichier à charger, zéro timeline à calculer. Le JS fait un aller-retour entre deux états, comme un interrupteur. C'est le CSS qui a la responsabilité de l'esthétique.

Le code complet

Deux blocs à ajouter à votre page : le CSS et le JS. Explication ligne par ligne juste après chacun.

Le CSS

<style>
  /* ANIMATION */
  section {
    scale: 0.5;
    opacity: 0.5;
    transition: all 500ms cubic-bezier(0.215, 0.61, 0.355, 1);
  }
  section.is--in-viewport {
    opacity: 1;
    scale: 1;
  } /* NO ANIMATION IN WEBFLOW */
  .wf-design-mode {
    section {
      scale: 1;
      opacity: 1;
    }
  }
</style>

Ligne par ligne :

  • section { scale: 0.5; opacity: 0.5; } définit l'état de départ : toutes les balises <section> commencent réduites de moitié et à moitié transparentes.
  • transition: all 500ms cubic-bezier(0.215, 0.61, 0.355, 1) indique au navigateur d'animer tout changement de propriété sur 500ms. Cette courbe correspond à un easeOutCubic classique (montée rapide puis ralentissement en douceur en fin de course), plus naturel qu'un ease par défaut ou qu'un linear.
  • section.is--in-viewport définit l'état d'arrivée : échelle et opacité normales. Dès que le JS ajoute cette classe, le navigateur anime automatiquement la transition entre les deux états. Aucun calcul supplémentaire à faire en JS.
  • .wf-design-mode section { scale: 1; opacity: 1; } neutralise l'animation dans l'éditeur Webflow. Détail juste après.
  • La syntaxe imbriquée .wf-design-mode { section { ... } } est du nesting CSS natif, supporté par tous les navigateurs modernes depuis 2023, pas du Sass ou du Less à compiler avant publication. Elle équivaut à écrire .wf-design-mode section { ... }.

bon à savoir

Pourquoi opacity et scale, et pas width ou margin ?

opacity et scale sont des propriétés que le navigateur peut animer directement sur le GPU, sans recalculer le layout de la page à chaque frame. Une transition sur width, height ou margin force au contraire le navigateur à recalculer la position de tous les éléments autour à chaque frame, ce qui coûte cher et peut créer des saccades. C'est ce détail qui rend la technique "quasi gratuite" en performance, pas juste l'absence de librairie.

Le JavaScript

<script>
  document.addEventListener('DOMContentLoaded', function () {
    const sections = document.querySelectorAll('section');
    function checkSections() {
      sections.forEach(function (section) {
        const rect = section.getBoundingClientRect();
        const isInViewport = rect.top <= window.innerHeight - 100;
        section.classList.toggle('is--in-viewport', isInViewport);
      });
    }
    window.addEventListener('scroll', checkSections, { passive: true });
    window.addEventListener('resize', checkSections);
    checkSections();
  });
</script>

Ligne par ligne :

  • document.querySelectorAll('section') récupère toutes les sections de la page, une seule fois, au chargement.
  • checkSections() fait tout le travail : pour chaque section, elle lit sa position avec getBoundingClientRect(), qui renvoie entre autres rect.top, la distance en pixels entre le haut de la section et le haut de la fenêtre visible.
  • rect.top <= window.innerHeight - 100 : la section est considérée "entrée" dans le viewport dès que son bord haut passe sous la hauteur de l'écran moins 100px. Ce -100 est une marge de déclenchement, elle permet de démarrer l'animation un peu avant que la section soit totalement visible, pour un rendu plus naturel. Ajustez cette valeur selon la hauteur de vos sections.
  • classList.toggle('is--in-viewport', isInViewport) ajoute ou retire la classe selon le résultat du calcul. Le deuxième argument du toggle évite d'écrire un if/else : la classe est posée si isInViewport est vrai, retirée sinon.
  • Les deux addEventListener relancent checkSections() à chaque scroll et à chaque resize. L'option { passive: true } sur l'écouteur de scroll dit au navigateur qu'on n'appellera jamais preventDefault() dedans, ce qui lui permet d'optimiser le scroll sans attendre le résultat du JS.
  • checkSections() est aussi appelé une première fois au chargement, pour que les sections déjà visibles à l'écran (typiquement le hero) s'affichent immédiatement, sans attendre un premier scroll.

Notez que ce n'est pas un déclenchement définitif : si l'utilisateur remonte au-dessus du seuil, la classe est retirée et l'animation repart dans l'autre sens. C'est cohérent dans les deux sens de scroll. Si vous voulez au contraire un déclenchement figé une fois joué, il faut ajouter une condition qui empêche de retirer la classe une fois posée, par exemple en sortant l'élément du tableau sections dès qu'il a atteint is--in-viewport une première fois.

Étendre la logique à d'autres éléments (textes, boutons, images)

Le JS ne connaît qu'un sélecteur : section. C'est le CSS qui décide de tout ce qui doit réagir à cette classe. Pour animer un titre, une image ou un bouton à l'intérieur d'une section déjà suivie, il n'y a rien à toucher côté JS : il suffit d'ajouter le sélecteur voulu dans la règle is--in-viewport, en ciblant l'enfant via un sélecteur descendant.

section {
  scale: 0.5;
  opacity: 0.5;
  transition: all 500ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
section.is--in-viewport {
  opacity: 1;
  scale: 1;
}

section h2,
section .card-image,
section .cta-button {
  opacity: 0;
  transform: translateY(20px);
  transition: all 600ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
section.is--in-viewport h2,
section.is--in-viewport .card-image,
section.is--in-viewport .cta-button {
  opacity: 1;
  transform: translateY(0);
}

/* NO ANIMATION IN WEBFLOW */
.wf-design-mode {
  section {
    opacity: 1;
    scale: 1;
  }

  section h2,
  section .card-image,
  section .cta-button {
    opacity: 1;
    transform: translateY(0);
  }
}

Chaque enfant peut même avoir sa propre animation (un translateY sur le titre, un scale sur l'image), tant que tout part du même déclencheur : la classe posée sur la section parente. Le seul cas où il faut toucher le JS, c'est si l'élément à animer n'est pas à l'intérieur d'une section suivie, un élément isolé plus haut dans la page par exemple. Il faudrait alors l'ajouter au querySelectorAll pour qu'il soit suivi de manière indépendante.

Ne pas oublier : désactiver l'animation dans l'éditeur Webflow

Sans la règle .wf-design-mode, vos sections resteraient à moitié transparentes et réduites en permanence dans l'éditeur Designer, puisque le JS qui déclenche is--in-viewport ne s'exécute pas de la même façon dans ce contexte. Résultat : vous travailleriez en permanence sur un design flou et minuscule.

wf-design-mode est une classe que Webflow ajoute automatiquement sur la balise <html> quand la page est affichée dans le Designer, pas en preview, pas en publié. En ciblant cette classe, on force scale: 1 et opacity: 1 uniquement dans ce contexte, sans toucher au comportement réel du site publié.

Un réflexe à prendre systématiquement dès qu'une animation dépend d'un état JS : si l'état par défaut, avant que le JS agisse, n'est pas le design "normal", il faut un garde-fou pour l'éditeur.

Où placer ce code dans Webflow

Deux options, selon si l'animation doit s'appliquer à tout le site ou à une seule page :

  • Tout le site : Site Settings → Custom Code → Header Code pour le CSS (le bloc <style>), Footer Code pour le JS (le bloc <script>). Le JS a besoin d'accéder au DOM une fois la page chargée, donc le Footer Code est le bon endroit.
  • Une seule page : Page Settings de la page concernée → section Custom Code → "Inside Head Tag" pour le CSS, "Before Body Tag" pour le JS.

Vous pouvez aussi tout coller dans un seul embed HTML personnalisé (Add Element → Embed) directement dans le Designer, mais dans ce cas le code n'est actif qu'à l'endroit où vous posez l'embed : moins pratique si la logique doit s'appliquer à toutes les sections de la page.

Les limites de la technique (et quand repartir sur une vraie lib)

Cette approche est volontairement simple, ce qui veut dire qu'elle a des limites assumées :

  • Pas de séquençage complexe. Impossible de dire "anime cet élément, puis celui-là 200ms après, puis reviens en arrière si l'utilisateur remonte". C'est un aller-retour entre deux états, pas une timeline.
  • Pas de scrub, cette animation liée en direct à la position de scroll façon effet parallax. Ici, l'animation se déclenche une fois le seuil franchi, elle ne suit pas le curseur de scroll en continu.
  • Un seul seuil de déclenchement (innerHeight - 100) pour toutes les sections. Gérer un seuil différent par élément demande d'adapter le JS, par exemple avec un attribut data- qui porte une marge personnalisée.
  • Un listener de scroll qui tourne en continu. Sur une page avec des centaines d'éléments à vérifier, ça peut devenir un point d'attention. À ce stade, un IntersectionObserver natif serait plus performant qu'un calcul manuel au scroll, tout en restant sans librairie externe.

Si le brief demande une timeline précise, un pin de section, un scrub lié au scroll ou une orchestration entre plusieurs éléments indépendants, c'est le signal pour repartir sur GSAP + ScrollTrigger plutôt que d'essayer de forcer cette technique au-delà de ce qu'elle sait faire proprement.

Publié le 10.09.2026

Ces tutoriels pourraient vous intéresser

tutoriels similaires

SEO / GEO

5 min lecture

5 vues

Comment faire une redirection dans Webflow en 2026 ?

Mis à jour le 19.12.2025 par Sandro DA SILVA

SEO / GEO

5 min lecture

5 vues

Ajouter des données structurées à votre site Webflow ?

Mis à jour le 21.08.2025 par Sandro DA SILVA

No-code

5 min lecture

5 vues

Comment obfusquer un lien dans Webflow ?

Mis à jour le 23.04.2025 par Sandro DA SILVA

Let’s f*****G GO !!

Prêt à faire décoller
votre activité  ?

Alexandre

Max

Enora

Bryan

Cannelle

Tiphaine

Vous allez :heart: notre collaboration...