Scroll✨ Premium

Reveal on Scroll

Cinq blocs de texte dans un cadre de 280 px qui défile ; un IntersectionObserver à 30 % ajoute la classe visible à l’entrée et la retire à la sortie — fondu et remontée de 30 px en 0,6 s, à chaque passage, dans les deux sens. L’élément observé est le conteneur au repos, l’animation est portée par un .reveal-inner intérieur, ce qui garantit un seuil stable quelle que soit la position de défilement.

IntersectionObserverCSSAnimation

Vous aurez accès à la démo interactive avec un compte gratuit.

Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Scroll comprend 70 effets, dont 12 gratuits. Découvrir la catégorie →

3 exemples d'usage

Paramètres d'un endpoint — documentation de référence d'une API — Reveal on Scroll exemple 1

① Paramètres d'un endpoint — documentation de référence d'une API

QuandPage de référence d'une API REST : en-tête de l'endpoint, onglets, puis la table des paramètres (nom, type, défaut, description) dans un cadre défilant de hauteur fixe.
PourquoiUn développeur remonte et redescend sans cesse dans une liste de paramètres. Comme la classe est retirée à la sortie, chaque ligne qui revient se rejoue dans le même état lisible ; sur un écran étroit en portrait, les lignes passées ne s'empilent pas — une révélation définitive (fx-0587, cartes collantes qui s'accumulent) noierait les lignes actives sous les précédentes.
RéglagesCode vendu tel quel (#reveal-scroll, [data-reveal], seuil 0,3) ; espaceurs remplacés par une grille de lignes de 62 px sans écart, cadre porté à 268 px ; défilement automatique aller-retour de la scène pour montrer l'entrée par le bas et le retour par le haut.
Prescriptions en cours — carnet de santé partagé sur tablette de cabinet — Reveal on Scroll exemple 2

② Prescriptions en cours — carnet de santé partagé sur tablette de cabinet

QuandCarnet de santé d'un patient sur la tablette du cabinet : le remplaçant fait défiler les prescriptions actives (médicament, posologie, durée, prescripteur) dans un cadre de taille fixe.
PourquoiLe seuil de 30 % coupe une ligne dès qu'elle n'est plus qu'un fragment : une prescription à moitié sortie du cadre s'efface au lieu de rester lisible en bord d'écran — sur un terminal partagé entre deux consultations, une donnée de santé ne doit pas traîner à l'affichage. Le code fait exactement cela, à condition de corriger la boucle de la sortie haute qui ferait clignoter la ligne coupée.
RéglagesInstance propre (le code vendu n'accepte qu'un #reveal-scroll) avec les mêmes options root / threshold: .3 ; opacité et translation portées par un élément intérieur pour que l'observateur mesure une boîte immobile — 0 bascule au repos ; cartes de 78 px, texte du paragraphe à rgba(255,255,255,.62), cadre de 262 px.
Mesures d'une partition — application de solfège sur smartphone — Reveal on Scroll exemple 3

③ Mesures d'une partition — application de solfège sur smartphone

QuandApplication de formation musicale : les mesures d'un extrait défilent dans un cadre étroit, une ou deux mesures à l'écran, l'apprenant remonte pour rejouer la précédente.
PourquoiRejouer, c'est remonter : la révélation réversible fait revenir la mesure précédente exactement comme la première fois, glissement compris, et la mesure suivante se retire dès qu'elle n'est plus qu'à 30 % — une seule mesure en plein focus sur 6 pouces, là où des mesures qui resteraient affichées rendraient la portée illisible.
RéglagesInstance propre, seuil 0,3, cadre de 300 px dans un écran de téléphone de 214 px de large ; portée en repeating-linear-gradient, têtes de notes en éléments absolus et hampes en pseudo-éléments ; translation réduite à 20 px (la bande instable a la largeur de la translation) et élément intérieur animé ; durée 0,5 s.

Comment ça marche

Le HTML vendu est un cadre .scroll-container#reveal-scroll (height: 280px, overflow: hidden auto, scroll-behavior: smooth, barre de 4 px stylée en ::-webkit-scrollbar) qui contient six .reveal-spacer de 80 px en alternance avec cinq .reveal-item portant l'attribut data-reveal : un trait .reveal-accent de 40 × 3 px (dégradé #22d3ee → #6366f1), un h4 de 0,9 rem en 700 blanc, un p de 0,75 rem à 45 % de blanc. Le premier bloc porte déjà visible dans le HTML. Au repos, .reveal-item .reveal-inner est à opacity: 0 et transform: translateY(30px) avec transition: .6s cubic-bezier(.16, 1, .3, 1) — le raccourci sans propriété, donc all ; .reveal-item.visible .reveal-inner remet opacité 1 et translation 0. .reveal-item lui-même n'a aucune transformation : il reste à sa position naturelle pour que l'observateur mesure toujours le même rectangle. Sous le cadre, .scroll-hint-label (« Scrollez pour explorer », 0,65 rem en capitales) pulse de 0,3 à 0,7 d'opacité toutes les 2 s, en position: absolute; bottom: 8px par rapport au wrapper .demo-preview. Mesuré sur une page vierge de 1 280 px : contenu de 1 052 px, course de 772 px, blocs de 114 px ; à 390 px : 1 148 px, course 868, blocs de 134 px.

Le script crée un seul IntersectionObserver avec root: revealScroll (le cadre, pas la fenêtre), threshold: .3 et rootMargin: "0px", et observe tous les [data-reveal] du document ; à chaque entrée, isIntersecting vrai ajoute visible, faux la retire. Mesuré à la molette (paliers de 60 px, Chromium, Firefox et WebKit identiques) : ensembles visibles [0, 1] → [1] → [1, 2] → [2] → [2, 3] → [3] → [3, 4] → [4] à la descente, et la séquence inverse jusqu'à [0] à la remontée — chaque bloc se rejoue à chaque passage, c'est la bidirectionnalité annoncée. La transition : 53 % de l'opacité atteints à 84 ms, 83 % à 167 ms, 93 % à 250 ms, 99,9 % à 500 ms ; la translation passe de 30 à 14 px dans les 84 premières millisecondes — la courbe (.16, 1, .3, 1) fait presque tout le trajet en un quart de seconde et s'achève lentement. Un détail de conception compte pour le seuil : l'observateur mesure la boîte de .reveal-item, qui ne porte aucune transformation — l'animation est déléguée à .reveal-inner. Le seuil de 30 % s'applique donc à la position naturelle dans le document. Un bloc de 81 px : la classe arrive quand 24 px y sont entrés, et la classe part dès que moins de 24 px restent — symétrie exacte à l'entrée comme à la sortie. Mesuré dans Chromium et Firefox : sous 0,3, l'entrée arrive avec isIntersecting à faux.

Le code sépare délibérément l'élément observé de l'élément animé. .reveal-item est observé par l'IntersectionObserver : son rectangle correspond à sa place naturelle dans le document, sans aucune transformation. .reveal-inner est celui qui anime : il porte opacity : 0 et transform: translateY(30px) au repos, et bascule vers opacity : 1; transform: translateY(0) quand le sélecteur .reveal-item.visible .reveal-inner est actif. Cette séparation garantit que le rectangle mesuré par l'observateur ne varie jamais avec l'état d'animation — le seuil de 30 % s'applique à la même boîte qu'un bloc soit à mi-course ou au repos. Sans cette séparation, tout déplacement de la boîte observée (ici 30 px vers le bas quand la classe est retirée) peut repousser l'élément dans le cadre et déclencher un nouveau passage du seuil — une boucle mesurée à 32 bascules par seconde dans Chromium, 7 dans Firefox, pour certaines positions de défilement. Le CSS de l'effet remet aussi à zéro les marges par défaut du navigateur sur h4 et p (margin: 0 0 6px et margin: 0) : sans cela, les blocs occupent plus de hauteur que prévu et la course de défilement change.

Accessibilité

  • prefers-reduced-motion : géré@media (prefers-reduced-motion: reduce) désactive la transition de .reveal-inner (transition: none) et la pulsation de l'étiquette (animation: none) : le contenu apparaît instantanément sans mouvement, l'étiquette reste visible mais immobile. Les blocs non encore visibles restent à opacity: 0 tant que l'observateur ne les juge pas à 30 % — ils sont néanmoins lus par les lecteurs d'écran car ils ne sont jamais en display: none.
  • Contraste mesuré sur #0a0a0f : titre #fff = 19,8:1 ; paragraphe rgba(255,255,255,.45) = 4,5:1 tout juste, à 12 px (0,75 rem) — passez à .55 (6,3:1) ou .6 (7,3:1). L'étiquette « Scrollez pour explorer » varie de 2,6:1 à 9,7:1 au fil de sa pulsation : décorative, gardez-la aria-hidden. Sur une page claire, le paragraphe blanc à 45 % est invisible (1:1) : les couleurs sont écrites pour un fond sombre.
  • Les blocs cachés sont à opacity: 0, jamais en display: none ni visibility: hidden : un lecteur d'écran lit les cinq blocs quel que soit le défilement, et un lien placé dans un bloc invisible resterait dans l'ordre de tabulation. Le cadre lui-même n'a pas de tabindex : donnez-lui tabindex="0" et un aria-label pour qu'il se parcoure au clavier partout ; les h4 supposent une hiérarchie h1–h3 au-dessus. Le texte vendu est en français sans accents (« Systeme », « Accessibilite », « reutilisables »).

Compatibilité navigateur

Requiert IntersectionObserver avec l'option root, ES2015 (const, fonctions fléchées, NodeList.forEach) et la forme à deux valeurs overflow: hidden auto — un navigateur qui ne la comprend pas ignore la déclaration et le cadre ne défile plus. Pas d'animation-timeline ni de scroll-timeline : la révélation vient d'un observateur JavaScript et d'une transition CSS classique, elle fonctionne donc sur Firefox (vérifié : mêmes bascules, même chronologie), là où les effets de la catégorie pilotés au scroll en CSS natif n'y font rien. Zéro dépendance.

Chrome 68+✓ Complet
Firefox 61+✓ Complet (barre de défilement native)
Safari 12.1+✓ Complet (défilement doux ignoré avant 15.4)
Edge 79+✓ Complet
Mobile iOS 12.2+✓ Complet (défilement tactile du cadre)
Android Chrome✓ Complet

Sans JavaScript, le cadre défile mais seul le premier bloc (qui porte visible dans le HTML) est lisible : les quatre autres restent à opacité 0. Sans IntersectionObserver (Safari < 12.1), même résultat — le script n'a pas de garde et lève une erreur. Sous prefers-reduced-motion, rien ne change : le code n'écoute pas la préférence.

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<div class="demo-preview">
  <div class="scroll-container" id="reveal-scroll">
    <div class="reveal-spacer"></div>
    <div class="reveal-item visible" data-reveal="">
      <div class="reveal-inner">
        <div class="reveal-accent"></div>
        <h4>…</h4>
        <p>…</p>
      </div>
    </div>
    <div class="reveal-spacer"></div>
    <div class="reveal-item" data-reveal="">
      <div class="reveal-inner">
        <div class="reveal-accent"></div>
        <h4>…</h4>
        <p>…</p>
      </div>
    </div>
    <div class="reveal-spacer"></div>
  </div>
  <span class="scroll-hint-label">Scrollez pour explorer</span>
</div>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Seuil (JS — threshold: .3) 0,3 Fraction de la boîte de .reveal-item qui doit être dans le cadre : mesuré, un bloc de 81 px apparaît quand 24 px y sont entrés et disparaît sous 24 px restants — seuil symétrique car l'élément observé n'a pas de transformation. .1 révèle dès le bord ; .6 attend que le bloc soit bien engagé — et un bloc plus haut que le cadre ne franchirait jamais 0,6.
Marge du cadre (JS — rootMargin: "0px") 0px Décale la zone de détection sans toucher au CSS : "0px 0px -60px 0px" retarde l'entrée par le bas de 60 px ; une valeur positive révèle avant l'arrivée. Avec root défini, la marge s'applique aux bords du cadre, pas de la fenêtre.
Amplitude (CSS — transform: translateY(30px)) 30 px Course de la remontée de .reveal-inner. 12px pour un glissement plus discret ; 0 ou none pour un fondu seul, sans déplacement. Cette valeur n'affecte pas la géométrie de .reveal-item mesuré par l'observateur.
Durée et courbe (CSS — transition: .6s cubic-bezier(.16, 1, .3, 1)) 0,6 s, sortie très rapide Mesuré : 53 % à 84 ms, 93 % à 250 ms, fin réelle vers 500 ms. Le raccourci vaut all : écrivez transition: opacity .6s …, transform .6s … pour ne pas animer d'autres propriétés par accident. ease-out .4s donne une arrivée plus linéaire.
Cadre et espaceurs (CSS — .scroll-container 280px, .reveal-spacer 80px) 280 px, 6 × 80 px Le cadre doit rester plus bas que son contenu (sinon rien ne défile, rien n'entre ni ne sort). Les espaceurs isolent chaque bloc ; supprimez-les pour une liste dense — plusieurs blocs sont alors visibles à la fois et n'entrent qu'à 30 % chacun.
Sélection des éléments (JS — document.querySelectorAll("[data-reveal]")) tout le document Mesuré : un élément [data-reveal] placé hors du cadre perd sa classe visible (jamais en intersection avec le root). Remplacez par revealScroll.querySelectorAll("[data-reveal]") pour rester dans le cadre, et changez l'id par instance.
Révélation définitive (JS — la branche classList.remove) réversible Remplacez e.target.classList.remove("visible") par rien, et ajoutez observer.unobserve(e.target) après l'ajout : chaque bloc n'entre qu'une fois et reste. C'est aussi la fin de la boucle de sortie haute — il n'y a plus de sortie.
Textes (HTML — h4, p, .scroll-hint-label) français sans accents Cinq titres et paragraphes de démonstration et l'étiquette « Scrollez pour explorer » sont dans le HTML : remplacez-les ; l'étiquette dépend du wrapper .demo-preview en position: relative — sans lui, mesuré, elle tombe au bas de la page.

FAQ

Pour que la boîte mesurée par l’IntersectionObserver ne change jamais de dimensions ni de position selon l’état de l’animation. Si .reveal-item portait lui-même transform: translateY(30px), le retrait de la classe en ferait descendre la boîte de 30 px — potentiellement de nouveau au-dessus du seuil de 30 %, ce qui rajouterait la classe, ferait remonter la boîte, la ferait repasser sous le seuil, etc. : une boucle mesurée à 32 bascules par seconde dans Chromium. .reveal-inner anime l’apparence sans jamais toucher à la boîte que l’observateur mesure, ce qui garantit 0 bascule à toute position de repos.
Ce sont deux mécaniques opposées. Card Stack est du CSS pur : animation-timeline: view() lie l'entrée de chaque carte à sa position dans le cadre, et position: sticky les fait s'empiler à 10, 20, 30 px du haut — les cartes passées restent à l'écran, c'est le principe, et Firefox ignore animation-timeline. Reveal on Scroll est un observateur JavaScript qui bascule une classe à 30 % : la transition est chronométrée (0,6 s) et non liée au défilement, un bloc sorti du cadre disparaît, et l'ensemble fonctionne dans Firefox, mesuré identique à Chromium. Prenez Card Stack pour un empilement scénographique de cinq cartes ; prenez Reveal on Scroll pour une liste où les éléments passés doivent laisser la place aux suivants — après avoir corrigé la boucle de sortie haute.
Pas avec le code tel quel : document.getElementById("reveal-scroll") ne prend que le premier cadre, et document.querySelectorAll("[data-reveal]") ramasse tous les blocs du document pour les observer par rapport à ce seul cadre. Mesuré : un [data-reveal] placé hors du cadre perd immédiatement sa classe visible, puisqu'il n'est jamais en intersection avec ce root — sur la page catalogue du site, le même sélecteur attrape d'ailleurs les cartes datées data-reveal="2026-…" du calendrier de révélation. Pour plusieurs instances, bouclez sur document.querySelectorAll(".scroll-container"), créez un observateur par cadre avec root: cadre, et observez cadre.querySelectorAll("[data-reveal]"). L'observateur n'est jamais déconnecté : dans une application qui monte et démonte le cadre, gardez la référence et appelez disconnect() au démontage.