Scroll Snap Sections
Un conteneur de 280 px en scroll-snap-type: y mandatory et scroll-behavior: smooth, cinq sections de 280 px, et 342 octets de JavaScript qui allument le point Math.round(scrollTop / 280) à chaque scroll. Les cinq points de 6 px sont des indicateurs : le code vendu ne contient aucun écouteur de clic — l'accès direct à une section se rajoute en six lignes.
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 : le wrapper .demo-preview (position: relative, min-height: 300px, flex centré, fond #0a0a0f, font-family: sans-serif) contient .snap-container#snap-scroll — 100% × 280px, overflow-y: auto, scroll-snap-type: y mandatory, scroll-behavior: smooth, ascenseur de 4 px stylé par ::-webkit-scrollbar — et cinq .snap-section de 280px en scroll-snap-align: start (colonne flex centrée, gap: 10px, padding: 24px), soit 1 400 px de contenu pour 1 120 px de course. Chaque section porte une icône .snap-icon de 48 px (SVG de 24 px, rayon 14 px), un .snap-title (1rem, 700) et un .snap-desc (.75rem, blanc à 40 %, max-width: 220px) ; les fonds sont cinq dégradés à 135° attribués par :nth-child(1) à (5) (#0f0f23 → #1a1040, #0f1a23 → #102a3e, #1a0f1a → #3e102a, #0f230f → #103e10, #23200f → #3e3510), l'icône reprenant la teinte de sa section (#818cf8, #22d3ee, #ec4899, #34d399, #fbbf24 sur fond à 20 %). À droite, .snap-dots#snap-dots — position: absolute, right: 12px, top: 50%, colonne, gap: 6px, z-index: 10 — aligne cinq .st-snap-dot de 6px en blanc à 15 % (transition: .3s) ; la classe .active les passe en #a855f7, halo 0 0 8px rgba(168, 85, 247, .5), scale(1.3). En bas, .scroll-hint-label « Scrollez pour explorer » (.65rem, capitales, bottom: 8px) pulse entre 30 % et 70 % d'opacité toutes les 2 s (@keyframes pulseHint, infini).
L'accrochage est entièrement CSS ; le script ne fait qu'observer. Mesuré dans Chromium : un coup de molette de 60 ou 120 px entame le mouvement (21 puis 52 px à 120 ms) et revient à 0, un coup de 280 px passe à la section suivante en 280 ms ; à 1 120 px, un coup de 500 px ne fait plus rien. Avec mandatory, les seules positions de repos sont 0, 280, 560, 840 et 1 120 px : un scrollTop forcé à 140 est ramené à 0, à 141 il est poussé à 280. Le JS tient en une ligne utile : snapScroll.addEventListener('scroll', …) calcule s = Math.round(snapScroll.scrollTop / 280) et fait classList.toggle('active', o === s) sur chaque point — le point bascule donc à mi-course, quand le défilement lisse franchit 140 px, pas à l'arrivée. Un scrollTo programmé met 280 ms pour une section, 464 ms pour trois (840 px) et 540 ms pour toute la course (1 120 px). L'écouteur est unique et porté par le conteneur : il n'y a rien à démonter, il disparaît avec l'élément. Le clavier fonctionne sans code : Tab donne le focus au conteneur (Chromium comme Firefox rendent les zones défilantes focalisables), ↓ mène à 280 px, PageDown à 560, End à 1 120, et les points suivent ; un balayage tactile mesuré en émulation mobile arrive à 280 px, point 2 allumé.
Ce que le code ne fait pas, mesuré : un clic au centre du 4e point ne déplace rien — scrollTop 0 → 0, point actif inchangé. Les points sont des div de 6 × 6 px, cursor: auto, sans role ni tabindex, et le mot click n'apparaît pas dans le script : ce sont des témoins, comme le dit la fiche catalogue (« indicateur de pagination latéral »). Le 280 du JS n'est pas lu dans le DOM : avec des sections passées à 360 px, la section 3 allume le point 4, la 4 le point 5, la 5 n'en allume aucun. Le CSS vendu contient 18 règles étrangères sur 43 (1 489 octets sur 4 641, 32 %) : .sn-progress-dot.active (le point de Snap + Progress, fx-0597), .sticky-text-block.active, .split-screen-content.active, .snap-nav-dot.active — qui lit var(--primary), défini seulement dans le prélude .demo-preview —, puis 14 règles du customizer, des favoris et du panneau de code du site (.customizer-panel.active, .favorite-btn.active, .code-tab.active…). Aucune ne trouve d'élément chez vous : à supprimer, pas à corriger. Le script n'est pas isolé (un const snapScroll au niveau global) : collé deux fois dans une page, le second exemplaire meurt sur SyntaxError: Identifier 'snapScroll' has already been declared. Enfin, les titres vendus sont sans accents (« Rapidite », « Modularite », « Elegance », « Fiabilite ») alors que « sécurité » en a, et l'indicateur « Scrollez pour explorer » n'est jamais masqué : à 1 120 px il pulse encore (display: block, opacité mesurée .40) et chevauche de 10 px le bas du conteneur.
Accessibilité
- prefers-reduced-motion absent du code. Mesuré en émulation : la requête média correspond, mais
scroll-behaviorrestesmooth(11 px à 40 ms, 280 à l'arrêt),pulseHinttourne (animation-play-state: running) et la transition des points reste à .3 s. Ajoutez@media (prefers-reduced-motion: reduce) { .snap-container { scroll-behavior: auto } .scroll-hint-label { animation: none } .st-snap-dot { transition: none } }— l'accrochage lui-même n'est pas une animation, il peut rester. - Les points ne sont pas des commandes :
divde 6 px,cursor: auto, nirole, nitabindex, ni écouteur de clic. Si vous ajoutez la navigation (cf. Personnaliser), faites-en des<button type="button">avec unaria-label(« Section 3 sur 5 »),aria-current="true"sur l'actif et une zone de clic d'au moins 24 px (WCAG 2.5.8) parpaddingou pseudo-élément — 6 px, même ×1,3, reste une cible de 8 px. Contraste mesuré des points (composant d'interface, seuil 3:1) : inactif 1,53:1 sur la section 1, actif 4,77:1 sur la section 1 mais 3,08:1 sur la section 5 (fond ambre) ; passez l'inactif à 35 % de blanc. - Texte :
.snap-descest du blanc à 40 % en 12 px — mesuré 3,81:1 sur la section 1, 3,58:1 sur la 2, 3,34:1 sur la 5, sous le seuil de 4,5:1 (WCAG 1.4.3) ; à 60 % de blanc, toutes les sections dépassent 6:1. Les titres blancs mesurent 18,9:1. L'indicateur pulsant est illisible par construction (1,22:1 à 1,83:1 selon la phase) : gardez-le décoratif avecaria-hidden="true", ou rendez-le lisible et masquez-le à la dernière section. - Clavier et lecteurs d'écran : le conteneur reçoit le focus par Tab (mesuré dans Chromium et Firefox), les flèches, PageDown et End défilent et les points suivent ; posez tout de même
tabindex="0"et unaria-labelsur#snap-scrollpour les navigateurs qui ne focalisent pas les zones défilantes (Safari). Remplacezdiv.snap-titlepar un titre (h3) pour donner une structure aux cinq sections, mettezaria-hidden="true"sur les SVG et sur la colonne de points tant qu'elle n'est pas cliquable. Intégration : le parent de.snap-dotsdoit resterposition: relative(c'est.demo-previewdans le code vendu), sinon la colonne se cale sur la fenêtre.
Compatibilité navigateur
Requiert ES2015 (const, fonctions fléchées), scroll-snap-type / scroll-snap-align (spécification 2018), scroll-behavior: smooth et box-shadow ; ::-webkit-scrollbar n'est que cosmétique. Rien ici n'est piloté au scroll en CSS natif (animation-timeline) : c'est de l'accrochage CSS classique et un écouteur JS, donc l'effet s'exécute intégralement dans Firefox — mesuré Firefox 153 : y mandatory appliqué, molette de 200 px → 280 px, point 2 allumé, zéro erreur. Zéro dépendance.
Sans JavaScript, l'accrochage et le défilement lisse fonctionnent (tout est CSS) mais le point actif reste figé sur le premier, la classe active étant écrite dans le HTML. Sans scroll-snap (navigateurs antérieurs à 2018), le conteneur défile librement et les points suivent quand même, Math.round désignant la section la plus proche.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="demo-preview">
<div class="snap-container" id="snap-scroll">
<div class="snap-section">
<div class="snap-icon"><svg width="24" height="24" viewBox="0 0 24 24">…</svg></div>
<div class="snap-title">Rapidite</div>
<div class="snap-desc">…</div>
</div>
<div class="snap-section">…</div>
<div class="snap-section">…</div>
<div class="snap-section">…</div>
<div class="snap-section">…</div>
</div>
<div class="snap-dots" id="snap-dots">
<div class="st-snap-dot active"></div>
<div class="st-snap-dot"></div>
<div class="st-snap-dot"></div>
<div class="st-snap-dot"></div>
<div class="st-snap-dot"></div>
</div>
<span class="scroll-hint-label">Scrollez pour explorer</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 des sections (CSS — .snap-container et .snap-section height: 280px ; JS — / 280) |
280 px, trois occurrences | Les trois valeurs doivent bouger ensemble : mesuré avec 360 px en CSS et 280 dans le JS, la section 3 allume le point 4 et la section 5 n'en allume aucun. Remplacez le 280 du script par snapScroll.clientHeight et la hauteur devient libre — 100vh pour des sections plein écran. |
Nombre de sections (HTML — .snap-section × 5, .st-snap-dot × 5) |
5 | Une section vaut un point ; le script compte les points, pas les sections. Les dégradés et les teintes d'icône s'arrêtent à :nth-child(5) : une sixième section n'a pas de fond (le #0a0a0f du wrapper apparaît), ajoutez ses deux règles. |
Type d'accrochage (CSS — scroll-snap-type: y mandatory) |
y mandatory | Aucune position de repos entre deux sections : un coup de molette de 120 px revient au départ (mesuré). y proximity autorise l'arrêt en cours de route, et Math.round désigne alors la section la plus proche. |
Défilement lisse (CSS — scroll-behavior: smooth) |
smooth | 280 ms par section, 540 ms pour toute la course, le point basculant à mi-chemin (140 px). Retirez-le pour un changement instantané ; c'est aussi la propriété à neutraliser sous prefers-reduced-motion. |
Points (CSS — .st-snap-dot, .st-snap-dot.active) |
6 px, blanc 15 % → #a855f7, halo 8 px, scale(1.3), transition .3 s | Taille, couleur de repos et d'activation. 8 à 10 px se voient à distance ; l'inactif à 35 % de blanc atteint 3:1 sur les fonds sombres ; la couleur active peut reprendre la teinte d'icône de la section courante. |
| Clic sur les points (JS — à ajouter) | absent | Six lignes après l'écouteur existant : snapDots.forEach((d, i) => d.addEventListener('click', () => snapScroll.scrollTo({ top: i * 280, behavior: 'smooth' }))), avec cursor: pointer sur .st-snap-dot. Le point s'allume de lui-même à mi-course, par l'écouteur de scroll. |
Indicateur « Scrollez pour explorer » (HTML — .scroll-hint-label ; CSS — pulseHint 2 s) |
visible en permanence, pulsation 30 → 70 % | Texte libre. Pour le masquer à la dernière section, ajoutez dans l'écouteur hint.hidden = s === snapDots.length - 1 ; pour le masquer dès le premier geste, testez snapScroll.scrollTop > 0. |
Fonds et icônes (CSS — .snap-section:nth-child(n), .snap-section:nth-child(n) .snap-icon) |
5 dégradés à 135° + 5 teintes d'icône | Dix règles couplées par position. Un seul fond pour toutes les sections (.snap-section { background: … }) donne un panneau d'application ; les dégradés vendus conviennent à un deck ou une page de présentation. |
FAQ
scrollTop à 0 et le premier point allumé — le script (342 octets) n'écoute que le scroll du conteneur. La navigation s'ajoute en six lignes (cf. Personnaliser) et les points devraient alors devenir des <button>. Snap + Progress (fx-0597) livre ce clic d'origine (data-index → scrollTo), calcule l'index sur clientHeight et ajoute une barre de progression ; Vertical Snap (fx-0593) est en CSS pur, sans témoin. Choisissez celui-ci pour sa colonne de points en surimpression, centrée sur le bord droit, et son script minimal — en acceptant d'écrire le clic vous-même.scrollTop par la constante 280, sans lire le DOM. Mesuré avec des sections de 360 px : 720 / 280 = 2,57, arrondi à 3, donc la section 3 allume le point 4 ; la section 5 (1 440 px) donne 5, index qui n'existe pas, et plus rien n'est allumé. Remplacez la constante par snapScroll.clientHeight (la hauteur visible du conteneur, égale à celle d'une section) et la même règle vaut pour 280 px, 360 px ou 100vh ; c'est ce que fait fx-0597.const snapScroll au niveau global et vise #snap-scroll et #snap-dots, deux ids. Mesuré : le second exemplaire s'arrête sur SyntaxError: Identifier 'snapScroll' has already been declared et ne s'exécute pas. Enveloppez le script dans une fonction immédiatement invoquée et remplacez les ids par un attribut : document.querySelectorAll('[data-snap]').forEach(c => { const dots = c.parentElement.querySelectorAll('.st-snap-dot'); c.addEventListener('scroll', () => { const s = Math.round(c.scrollTop / c.clientHeight); dots.forEach((d, i) => d.classList.toggle('active', i === s)); }); }) — chaque bloc a alors ses points et sa hauteur.