Horizontal Scroll Section
Six panneaux de 200 px sur une piste épinglée dans une zone de 280 px qui défile sur 840 px : une timeline nommée (scroll-timeline: --hscrollTimeline) translate la piste de −75 % de sa propre largeur — −984 px pour 1 312 px, mesuré linéaire — sans pixel codé en dur ni JavaScript ; Firefox, qui n'anime pas au défilement, reçoit la piste à plat par un repli @supports.
Mis à jour le
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
Quatre boîtes emboîtées et une animation. La zone .hscroll-outer (100% × 280px, overflow: hidden auto) est le conteneur qui défile et déclare la timeline nommée : scroll-timeline: --hscrollTimeline y. Dedans, .hscroll-spacer fait 400% de la zone, 1 120 px : la course vaut 1 120 − 280 = 840 px. Le bloc .hscroll-sticky (position: sticky; top: 0, 280 px, overflow: hidden) reste épinglé en haut de la zone toute la course (mesuré : top à 0 de bout en bout), et la piste .hscroll-track — flex, gap: 16px, padding: 20px 16px, width: max-content, 1 312 px pour six panneaux de 200 px — porte l'animation scrollHorizontal (translateX(0) → translateX(-75%)) avec animation-timeline: --hscrollTimeline, animation-duration: auto, linear et animation-fill-mode: both. Mesuré dans Chromium par crans de 210 px : −246, −492, −738, −984 px à 840, soit 1,17 px de piste par pixel défilé, linéaire ; molette en arrière, la piste revient à 0. La timeline nommée se résout vers l'ancêtre le plus proche qui la déclare : deux copies collées dans la même page sont indépendantes (mesuré : molette sur la seconde, −492 px pour elle, 0 pour la première). Champ JavaScript vide, aucune erreur console, aucune variable du site.
Le -75% est le cœur de l'effet : un pourcentage de translateX se rapporte à la largeur de l'élément transformé, donc à la piste. Ajoutez un panneau, la piste passe à 1 528 px et la translation à −1 146 px sans toucher au CSS (mesuré : septième panneau entier de 166 à 366 px en bout de course). C'est ce qu'une valeur en pixels ne fait pas — Horizontal Scroll (fx-0564) fixe -600px. L'envers : ce qui reste visible à la fin est toujours le dernier quart de la piste, 328 px, quelle que soit la largeur du cadre — à 1 264 px de large, 952 px restent vides à droite (voir la FAQ). Les trois scènes de cette page donnent donc à la piste quatre fois la largeur du cadre : panneaux de 341 px pour 540 px, et le dernier panneau s'arrête au bord.
Le code vendu a été corrigé sur deux points. La zone était en height: 100% : dans un parent sans hauteur fixe (.demo-preview n'a qu'un min-height), ce pourcentage retombe sur auto, la zone prend la hauteur de son contenu, la course vaut 0 et la piste ne bouge jamais — mesuré collé dans une page vierge à 1 280 et 390 px, et dans l'aperçu du site lui-même. Elle est désormais à 280px, la hauteur du bloc épinglé. Et un bloc @supports not (animation-timeline: scroll()) a été ajouté pour les navigateurs sans animations pilotées par le défilement, Firefox en tête (voir la compatibilité). Reste un élément étranger : l'indice .slr-scroll-hint « Scrollez verticalement » (préfixe d'un autre effet, français en dur, 11,2 px, blanc à 40 %, rebond de 4 px toutes les 2 s), en position: absolute à 8 px du bas de .demo-preview — hors de ce cadre, il tombe au bas de la page.
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, rien ne change — −984 px en bout de course, indice en rebond (
animation-play-state: running). Le mouvement horizontal ne se produit qu'à la demande du lecteur, mais WCAG 2.3.3 vise aussi les animations déclenchées par l'interaction : ajoutez@media (prefers-reduced-motion: reduce) { .slr-scroll-hint { animation: none } }et, pour supprimer la conversion d'axe, reprenez dans cette media query les trois règles du bloc@supports not(spacer enauto, sticky enstatic+overflow-x: auto,animation: none) : la piste se lit alors à plat, en défilement horizontal natif. - Clavier : mesuré dans Chromium, Tab donne le focus à la zone sans
tabindex(anneauoutline: auto 1px) ; flèche bas avance de 40 px (piste à −46,9 px), Page suivante de 285 px (−334 px), Fin va au bout (−984 px), Début revient à 0. Firefox et Safari ne focalisent pas une zone défilante sanstabindex="0": ajoutez-le sur.hscroll-outer, avec unaria-labelet un style:focus-visible. Au doigt, le geste doit commencer dans les 280 px de la zone ;overscroll-behavior: containévite d'entraîner la page en butée. - Contraste (texte blanc de 19,2 px en graisse 700 : texte large, seuil 3:1) : les panneaux 1 (4,5:1 et 4,2:1) et 6 (5,7:1 et 4,7:1) tiennent, le 2 de justesse (3,5:1 et 3,7:1) ; les 3, 4 et 5 échouent sur leur première couleur — 2,4:1 sur
#06b6d4, 2,2:1 sur#f59e0b, 2,5:1 sur#10b981. Assombrissez ces teintes de départ ou posez une ombre portée sur le texte. L'indice à 40 % de blanc vaut 3,8:1 pour 11,2 px : sous 4,5:1. - Lecteurs d'écran et repli : six
divsans titre, lus dans l'ordre du DOM quelle que soit la position de la piste — rien n'est masqué à la synthèse vocale, mais rien ne structure non plus : donnez un titre (h3ouh4) à chaque panneau et unaria-labelà la zone. L'indice « Scrollez verticalement » est lu en français quelle que soit la langue de la page :aria-hidden="true". Dans le repli sansanimation-timeline, c'est le bloc.hscroll-stickyqui défile horizontalement : il a besoin du mêmetabindex="0"pour le clavier.
Compatibilité navigateur
CSS seul : scroll-timeline nommée, animation-timeline, animation-duration: auto, position: sticky, overflow: hidden auto (deux valeurs), width: max-content, flexbox, dégradés. Zéro JavaScript, zéro dépendance. Les animations pilotées par le défilement existent dans Chrome et Edge 115+, Safari et iOS 26 (mesuré dans le WebKit de Playwright : −984 px en bout de course) ; Firefox ne les exécute dans aucune version : mesuré dans Firefox 153, CSS.supports('animation-timeline: scroll()') renvoie faux et, sans repli, l'animation saute à son état final — piste figée à −984 px, panneaux 1 à 4 hors cadre, seuls le 6 et la moitié du 5 visibles. Le bloc @supports not (animation-timeline: scroll()) du code vendu prend alors le relais : mesuré, les six panneaux à leur place (16 à 1 296 px), zone sans course verticale, bloc .hscroll-sticky en position: static et overflow-x: auto — 48 px de course horizontale à 1 264 px de large, 938 px à 374 px, molette horizontale et Maj+molette fonctionnent. Un visiteur Firefox lit une rangée de panneaux qui glisse au doigt ou à la molette, sans piste épinglée.
Avant Chrome/Edge 115 et Safari 26, le même bloc @supports not s'applique : piste à plat, comme dans Firefox (@supports existe depuis Chrome 28, Firefox 22 et Safari 9). Sans la forme à deux valeurs overflow: hidden auto (Chrome < 68, Firefox < 61, Safari < 12.1), la zone ne défile plus verticalement — mais ces versions n'ont pas animation-timeline non plus, le repli s'applique et rien n'est perdu. Sans position: sticky (avant 2017), le bloc remonte avec le plateau et sort de la zone après 280 px.
Le code
Structure HTML à coller dans votre page (le CSS + JS sont inclus avec Premium) :
<div class="demo-preview">
<div class="hscroll-outer">
<div class="hscroll-spacer">
<div class="hscroll-sticky">
<div class="hscroll-track">
<div class="hscroll-panel">…</div>
<div class="hscroll-panel">…</div>
<div class="hscroll-panel">…</div>
<div class="hscroll-panel">…</div>
<div class="hscroll-panel">…</div>
<div class="hscroll-panel">…</div>
</div>
</div>
</div>
</div>
<span class="slr-scroll-hint">…</span>
</div>
Le code complet HTML + CSS + JS de Horizontal Scroll Section, prêt à coller — et les 811 effets. Ou 299 € une fois, accès à vie.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Course (CSS — .hscroll-spacer { height: 400% }, .hscroll-outer { height: 280px }) |
plateau de 1 120 px, course 840 px (1,17 px de piste par pixel) | La course vaut plateau − zone, trois fois la hauteur de la zone à 400 %. 600% donne 1 400 px de course et une piste deux fois plus lente sous la molette. Le bloc épinglé doit garder la hauteur de la zone : plus petit, une bande de plateau nu apparaît dessous ; plus grand, il est rogné. |
Translation finale (CSS — @keyframes scrollHorizontal { to { transform: translateX(-75%) } }) |
−75 % de la piste (−984 px pour 1 312 px) | Le pourcentage se rapporte à la piste : à la fin, son dernier quart reste visible (328 px), quel que soit le cadre. -50% arrête la piste à mi-chemin, sur les panneaux 4 à 6. Pour caler la fin sur le bord du cadre : translateX(calc(100cqw - 100%)) avec container-type: inline-size sur .hscroll-sticky (mesuré : dernier panneau à 16 px du bord, à 1 264 comme à 374 px de large). |
Nombre et taille des panneaux (HTML + CSS — .hscroll-panel { width: 200px; height: 200px }, gap: 16px) |
6 panneaux de 200 × 200 px, piste de 1 312 px | Chaque panneau ajouté allonge la piste de 216 px et la translation de 162 px, rien à recalculer (mesuré à 7 : piste 1 528 px, −1 146 px). Un septième n'a pas de fond : ajoutez sa règle :nth-child(7). Pour une fin de course calée sur un cadre de largeur W avec six panneaux : largeur = (4 W − 112) ÷ 6, soit 341 px pour 540 px. |
Progression (CSS — animation-timing-function: linear, animation-range: normal) |
linéaire sur toute la course | ease-in-out fait démarrer et finir la piste doucement pour le même défilement. animation-range: 10% 90% laisse la piste immobile sur les 84 premiers et 84 derniers pixels de course, animation-fill-mode: both tenant les deux états — le temps de lire le premier panneau. |
Fonds des panneaux (CSS — :nth-child(1) à (6)) |
six dégradés à 135° : indigo, rose, cyan, ambre, vert, violet | Un dégradé par panneau, dans l'ordre du DOM ; remplacez-les par une image (background: url(…) center/cover) ou une couleur unie. Le texte blanc de 19,2 px passe sous 3:1 sur les teintes claires des panneaux 3, 4 et 5 : assombrissez la première couleur du dégradé. |
Indice « Scrollez verticalement » (CSS — .slr-scroll-hint, @keyframes hintBounce) |
8 px du bas de .demo-preview, 11,2 px, blanc à 40 %, rebond de 4 px toutes les 2 s |
Français en dur, préfixe d'un autre effet, ancré sur .demo-preview : hors de ce cadre, il descend au bas de la page. Supprimez le span et la @keyframes hintBounce avec lui, ou posez-le dans .hscroll-sticky (position: relative sur le bloc) pour qu'il suive la zone. |
Repli sans animation-timeline (CSS — @supports not (animation-timeline: scroll())) |
piste à plat, défilement horizontal natif du bloc | Pour montrer tous les panneaux d'un coup plutôt qu'une rangée à faire glisser : dans ce bloc, .hscroll-track { flex-wrap: wrap; width: auto; animation: none } et .hscroll-sticky { height: auto } — les six panneaux se rangent en grille. Pour un carrousel à crans : scroll-snap-type: x mandatory sur le bloc et scroll-snap-align: start sur les panneaux. |
FAQ
translateX(-75%) se mesure sur la piste, pas sur le cadre : en bout de course, il en reste toujours le dernier quart, 328 px pour six panneaux de 200 px. Mesuré à 1 264 px de large : le panneau 6 occupe 112 à 312 px, la moitié du 5 le précède, et 952 px sont vides à droite — la piste a « trop » avancé pour ce cadre ; à 374 px, le même état laisse 62 px libres. La valeur est calibrée pour un cadre d'environ 328 px, celui de la carte du catalogue, où le dernier panneau arrive pile au bord. Deux corrections : dimensionner les panneaux pour que la piste fasse quatre fois le cadre (341 px pour 540 px, les scènes de cette page), ou écrire la fin de course en fonction du cadre, translateX(calc(100cqw - 100%)) avec container-type: inline-size sur .hscroll-sticky — mesuré, le dernier panneau s'arrête à 16 px du bord à 1 264 comme à 374 px, sans changer la course de défilement.translateX(-600px), une valeur fixe : une sixième carte finit à 400 px du bord gauche, hors d'un cadre de 374 px, et la valeur est à recalculer à chaque carte ajoutée ; fx-0567 écrit -75% et absorbe un septième panneau sans rien changer (mesuré : entier, de 166 à 366 px). fx-0564 utilise la timeline anonyme animation-timeline: scroll(), qui prend le premier ancêtre défilant — emballez la piste dans une autre boîte à overflow: auto et c'est elle qui pilote ; fx-0567 nomme sa timeline sur la zone et la piste la retrouve à travers n'importe quel intermédiaire. fx-0564 épingle sa piste à top: 50% dans un plateau de 800 px (course 520) avec scroll-behavior: smooth ; fx-0567 épingle un bloc plein de 280 px à top: 0 sur 840 px, sans lissage, en animation-fill-mode: both. Ni l'un ni l'autre ne bouge dans Firefox ; seul fx-0567 y prévoit un repli. Sticky Horizontal Reveal (fx-0796) est le voisin le plus proche : même spacer, même bloc collant, même rangée, mais déplacée par un script — IIFE de 1,4 Ko, un écouteur scroll par scène, --shr-x réécrite dans le DOM à chaque cran. Prenez fx-0796 si la rangée doit bouger aussi dans Firefox, s'arrêter pile sur le dernier panneau quel qu'en soit le nombre (course calculée sur la largeur réelle de la rangée) ou alimenter une barre de progression (--shr-p) ; prenez fx-0567 pour une piste sans aucun JavaScript, pilotée par la feuille de style sans écouteur ni écriture dans le DOM, avec un repli Firefox déjà dans le CSS vendu. Les deux se passent de bibliothèque et d'image, acceptent plusieurs copies dans la même page et reculent avec la molette..hscroll-outer { height: auto; overflow: visible; scroll-timeline: none }, cadre .demo-preview en display: block), donnez au plateau la course en hauteurs d'écran et une timeline de vue (.hscroll-spacer { height: 400vh; view-timeline: --hsView block }), au bloc épinglé la hauteur de l'écran (.hscroll-sticky { height: 100vh }), et branchez la piste : .hscroll-track { animation-timeline: --hsView; animation-range: contain 0% contain 100% }. La plage contain couvre exactement la phase épinglée : mesuré, le bloc se fixe à 608 px de défilement et se libère à 3 008 (trois fois 100vh), la piste va de 0 à −984 px sur cet intervalle (−489 px à mi-course) et y reste ensuite grâce au fill-mode: both. Une scroll-timeline nommée sur html étalerait la course sur tout le document, contenu avant et après compris. Le repli @supports not reste valable tel quel.