Smooth Snap
Quatre sections de 280 px sous scroll-snap-type: y mandatory et scroll-snap-stop: always ; un IntersectionObserver (seuil 0,6, soit 168 px visibles) pose la classe in-view sur la section active et une cascade de 700 ms révèle icône, titre et texte. Rien n'est prévu pour prefers-reduced-motion, et le stop n'a fait aucune différence mesurable sur le banc.
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



Comment ça marche
Anatomie : #snSmoothContainer (.sn-smooth-container, 100% × 280px, overflow-y: auto, scroll-snap-type: y mandatory, scroll-behavior: smooth) contient quatre .sn-smooth-section de 280px — exactement la hauteur du conteneur — avec scroll-snap-align: start et scroll-snap-stop: always : 1 120 px de contenu, 840 px de course, trois paliers. Chaque section est une colonne flex centrée (gap: 12px, padding: 20px) sur le même dégradé #0f0f23 → #1a1a3e — rien ne distingue visuellement deux sections, hormis leur contenu : un emoji .sn-smooth-icon (2rem), un h4 (.9rem, 700) et un p (.72rem, blanc à 60 %), tous trois à opacity: 0 et translateY(20px) au repos. Le rappel .sdc-scroll-hint (« Scrollez ↓ », 10,4 px, blanc à 30 %, pulsation de 2 s) est hors du conteneur, en absolu dans .demo-preview (position: relative).
Le script (IIFE, ES5) prend le conteneur par getElementById — une seule instance par page — et crée un IntersectionObserver avec root: container et threshold: 0.6 ; le rappel ajoute in-view à toute section isIntersecting et la retire aux autres. Le CSS fait le reste : .sn-smooth-section.in-view .sn-smooth-icon, h4 et p passent à opacity: 1 et translateY(0) avec des transitions de .5s, .5s .1s et .5s .2s — une cascade de 700 ms (mesuré : icône à 1 vers 500 ms, titre vers 600 ms, texte à 0,999 à 700 ms). Le seuil 0,6 de 280 px vaut 168 px : une section gagne la classe dès que 168 px en sont visibles et la perd en dessous. Pendant un passage, la section qui part la perd après 112 px de déplacement et celle qui arrive ne la gagne qu'à 168 px : sur 56 px, aucune section n'est in-view (mesuré : positions 113 à 167 px). Un scrollTo lissé de 280 px se pose en 271 ms ; la classe quitte la section 1 à 134 px (79 ms) et atteint la section 2 à 180 px (96 ms), soit 17 ms sans contenu. Au chargement, le premier rappel de l'observer pose la classe sur la section 1 (mesuré 15 ms après DOMContentLoaded) : le in-view figé dans le HTML vendu est redondant tant que le script tourne.
Le snap, mesuré dans Chromium headless (macOS) : un cran de molette de 100 à 139 px ne franchit pas le palier — le conteneur monte puis revient à 0 en 180 ms (mandatory choisit le point le plus proche, la moitié de 280) ; 141 px suffisent pour atteindre 280 ; Flèche bas, Page suivante et Espace font chacun une section, Fin saute directement à 840. Un événement molette de 600 px se pose à 560 (deux sections) et un élan tactile synthétisé de 600 px à 840 — exactement les positions de Vertical Snap (fx-0593), qui n'a pas scroll-snap-stop. La propriété n'a donc rien bloqué sur ce banc : la spécification la réserve aux défilements inertiels (élan tactile, molette animée) que l'automate ne reproduit pas, et elle ne contraint jamais la barre de défilement, la touche Fin ni scrollTo() — à vérifier sur l'appareil visé avant d'en faire une garantie. Le reste du code : aucune règle prefers-reduced-motion, aucun disconnect() (l'observer est local à l'IIFE), deux règles mortes .smooth-snap-section.in-view … et .smooth-icon héritées d'une version antérieure (classes absentes du HTML), un style="font-family: Inter…" en ligne sur les quatre h4 alors qu'Inter n'est pas chargée, et des textes livrés sans accents (« Demarrez », « idees », « Precision », « detail », « resultat »).
Accessibilité
- prefers-reduced-motion absent du code : mesuré avec la préférence active, le fondu joue tel quel (opacité de 0,02 à 1 entre 123 et 615 ms après l'entrée),
scroll-behavior: smoothreste actif et le rappel continue de pulser. Ajoutez@media (prefers-reduced-motion: reduce) { .sn-smooth-container { scroll-behavior: auto } .sn-smooth-section .sn-smooth-icon, .sn-smooth-section h4, .sn-smooth-section p { transition: none } .sdc-scroll-hint { animation: none } }: le contenu apparaît alors d'un coup à l'entrée, et le snap — qui n'est pas une animation — reste. - Clavier : mesuré dans Chromium, Tab donne le focus au conteneur sans
tabindex(défileurs focalisables au clavier), puis Flèche bas, Page suivante et Espace avancent chacun d'une section (280 px) et Fin va à la dernière (840). Ce focus implicite n'existe pas dans tous les navigateurs : poseztabindex="0", unaria-labelet un anneau de focus visible (:focus-visible { outline: 2px solid #818cf8 }) sur.sn-smooth-container. - Lecteurs d'écran : le contenu masqué l'est par
opacity: 0, qui ne retire rien de l'arbre d'accessibilité — les quatre sections sont lues dans l'ordre, snap ou pas, sansaria-hiddenà ajouter. En revanche la classein-viewn'est annoncée nulle part : si « a été vu » a un sens fonctionnel (consentement, règle lue), ne le déduisez pas de la seule classe. Lesh4sont des titres de niveau 4 sans h1–h3 au-dessus : alignez le niveau sur votre page. - Contraste (calculé sur le dégradé
#0f0f23 → #1a1a3e) : titre blanc 16,7 à 18,9:1, paragraphergba(255,255,255,.6)6,7 à 7,2:1 — conformes, mais le paragraphe fait 11,5 px. Le rappel « Scrollez ↓ » (rgba(255,255,255,.3)sur#0a0a0f) mesure 2,6:1 à 10,4 px : c'est une consigne, pas une décoration — passez l'opacité à.5(5,3:1) ou.6(7,3:1). - Intégration : la hauteur du conteneur et celle des sections doivent rester égales (le seuil 0,6 et le snap en dépendent) ; le rappel est positionné par rapport à
.demo-preview— sans ce parent enposition: relative, il tombe en bas de l'ancêtre positionné suivant. Un bloc de 280 px enmandatorycapte la molette au survol : trois crans avant de rendre la main à la page. Dans une application, gardez une référence à l'observer pour ledisconnect(), aujourd'hui impossible (variable locale).
Compatibilité navigateur
JavaScript ES5 (var, function) et CSS classique : IntersectionObserver avec root, scroll-snap-type, scroll-snap-align, scroll-snap-stop, scroll-behavior, gap en flex, transitions. Aucune animation-timeline : contrairement aux effets pilotés par le défilement en CSS natif, celui-ci fonctionne dans Firefox (103+ pour scroll-snap-stop, 63+ pour tout le reste). Zéro dépendance.
Sans JavaScript, le snap fonctionne (tout en CSS) mais seule la section 1, dont la classe in-view est écrite dans le HTML, affiche son contenu : les sections 2 à 4 restent vides (mesuré : opacité 0 sur leurs titres). Sans scroll-snap-stop (Firefox < 103, Safari < 15), snap et fondu fonctionnent, seule l'obligation d'arrêt disparaît. Sous prefers-reduced-motion, rien ne change.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="demo-preview">
<div class="sn-smooth-container" id="snSmoothContainer">
<div class="sn-smooth-section in-view">
<span class="sn-smooth-icon">🚀</span>
<h4>Lancement</h4>
<p>Démarrez avec style et élégance</p>
</div>
<div class="sn-smooth-section">
<span class="sn-smooth-icon">💡</span>
<h4>Innovation</h4>
<p>Des idées qui transforment</p>
</div>
<div class="sn-smooth-section">
<span class="sn-smooth-icon">🎯</span>
<h4>Précision</h4>
<p>Chaque détail compte</p>
</div>
<div class="sn-smooth-section">
<span class="sn-smooth-icon">✨</span>
<h4>Excellence</h4>
<p>Le résultat final parfait</p>
</div>
</div>
<span class="sdc-scroll-hint">Scrollez ↓</span>
</div>
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éfaut | Effet |
|---|---|---|
Hauteur (CSS — .sn-smooth-container et .sn-smooth-section) |
280px / 280px | Les deux doivent rester égales : la section remplit le cadre et le seuil 0,6 se calcule sur cette hauteur (168 px). 100vh des deux côtés pour un plein écran ; le rappel « Scrollez ↓ » reste à 8 px du bas de .demo-preview. |
Seuil de l'observer (JS — threshold: 0.6) |
0.6 (168 px visibles) | Moment où la classe bascule. 0.5 supprime la bande morte de 56 px (départ et arrivée à 140 px) ; 0.9 ne révèle le contenu qu'une fois le snap presque posé — la cascade démarre plus tard mais ne chevauche plus le mouvement. |
scroll-snap-stop (CSS — .sn-smooth-section) |
always | normal retrouve le comportement de fx-0593 : un élan peut traverser plusieurs sections. Sans effet sur le clavier, la barre de défilement ni scrollTo() — et aucune différence mesurée sur le banc headless. |
Cascade (CSS — transition: .5s, .5s .1s, .5s .2s) |
700 ms au total | Durée commune et décalages icône → titre → texte. .3s / .05s / .1s donne 400 ms, plus proche de la durée du snap (271 ms) ; la sortie utilise les mêmes valeurs, la section qui part s'efface donc à la même vitesse. |
Amplitude (CSS — transform: translateY(20px)) |
20 px | Course de la remontée, identique pour les trois éléments. 0 pour un fondu pur ; 40px pour une entrée plus marquée ; un scale(.96) ajouté au repos fait un léger zoom. |
scroll-behavior (CSS — .sn-smooth-container) |
smooth | Ne concerne que les déplacements programmés et clavier (mesuré : un cran de molette n'est pas animé par cette propriété). auto sous prefers-reduced-motion, ou si vous pilotez le conteneur depuis des boutons et voulez un saut net. |
Nombre et fond des sections (HTML — blocs .sn-smooth-section ; CSS — background) |
4, un seul dégradé #0f0f23 → #1a1a3e |
Ajoutez ou retirez des blocs : l'observer prend tous les .sn-smooth-section du conteneur. Les quatre partagent le même fond ; un :nth-child par section (comme fx-0593) rend le changement de palier visible avant même le fondu. |
Cible du script (JS — getElementById('snSmoothContainer')) |
un id, une instance | Pour plusieurs conteneurs, remplacez par document.querySelectorAll('.sn-smooth-container').forEach(init) et retirez l'id ; gardez la référence de chaque observer pour disconnect(). |
FAQ
always. Sur le banc, rien : molette de 600 px → 560 (deux sections), rafale trackpad de 30 × 40 px → 840, élan tactile synthétisé de 600 px → 840, touche Fin → 840 — les mêmes positions, à l'unité près, que fx-0593 sans la propriété. Chromium headless ne reproduit pas un vrai geste inertiel, et la propriété n'agit de toute façon ni sur la barre de défilement, ni sur le clavier, ni sur scrollTo(). La différence certaine entre les deux effets est ailleurs : fx-0593 est du CSS pur, muet ; fx-0598 pose et retire une classe in-view à 168 px de visibilité, avec un fondu de 700 ms — c'est ce signal, exploitable en CSS ou en JavaScript, qui justifie de le choisir.in-view est le crochet. En CSS : .sn-smooth-section.in-view .ma-maquette { … } lance n'importe quelle transition dans la section (barre qui se remplit, lignes qui glissent). En JavaScript : un MutationObserver sur attributeFilter: ['class'] du conteneur, ou une ligne ajoutée dans le rappel de l'observer (entry.target.dispatchEvent(new CustomEvent('sn:enter'))). Attention à la symétrie : le rappel retire la classe dès que la section passe sous 168 px visibles, soit après 112 px de déplacement — pour un état « déjà vu », mémorisez le premier ajout (ou appelez obs.unobserve(entry.target) à la première entrée) au lieu de lire la classe en direct.opacity: 0 tant que la classe in-view n'arrive pas, et que le script échoue en silence — aucune erreur console dans les quatre cas mesurés. Soit l'id a changé (getElementById('snSmoothContainer') rend null et l'IIFE sort au if (!container) return), soit le script est placé avant le HTML (il s'exécute immédiatement, le conteneur n'existe pas encore), soit deux conteneurs partagent l'id (seul le premier est observé), soit JavaScript est absent : la section 1 s'affiche grâce au in-view écrit dans le HTML, les autres jamais. Vérifiez l'ordre HTML puis script, l'unicité de l'id, et considérez la classe figée comme un repli, pas comme l'état initial.