Horizontal Scroll
Six vignettes de 180 × 140 px sur une piste de 1 172 px, dans un conteneur overflow: auto hidden à scroll-snap-type: x mandatory : chaque geste s'arrête sur une vignette centrée, les voisines dépassent des deux bords — 1,4 Ko de CSS, zéro JavaScript, aucune animation : le doigt ou la molette donnent la vitesse. Une racine .sb-horiz porte le fond sombre et ancre l'indice ; le conteneur est borné à 900 px pour que la piste dépasse toujours, même sur un grand écran.
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
Une racine, trois niveaux et une étiquette. .sb-horiz est la racine de l'effet : position: relative (l'ancre de l'indice), width: 100%, fond #0a0a0f, texte blanc, police sans-serif — collée dans une page blanche, elle apporte son propre fond au lieu de compter sur celui de la page. Dedans, .sb-horiz-content est le conteneur défilant : width: 100%, max-width: 900px, margin: 0 auto, height: 280px, overflow: auto hidden (défilement horizontal, rien en vertical), scroll-snap-type: x mandatory, display: flex et align-items: center. Il contient .sb-horiz-track, une piste flex de gap: 12px et padding: 20px 16px, qui aligne six .sb-horiz-item de 180 × 140 px (flex-shrink: 0, arrondi 14 px, chiffre blanc gras en 1,2 rem, scroll-snap-align: center). Chacune porte son dégradé à 135° en attribut style, et la chaîne se referme sur elle-même : la couleur de fin d'une vignette est celle du début de la suivante (#6366f1 → #8b5cf6 → #d946ef → #f43f5e → #f59e0b → #22c55e → #06b6d4). Piste : 6 × 180 + 5 × 12 + 2 × 16 = 1 172 px. Sous le tout, .sdc-scroll-hint « Scrollez → » : absolu dans la racine, 8 px du bas, 0,65 rem, blanc à 30 %, dont l'opacité oscille de .3 à .7 toutes les 2 s (sdcHintPulse). Le champ JavaScript est vide : la mécanique entière est celle du navigateur.
Le snap. À la fin de chaque geste, le navigateur pose le défilement sur une position d'arrêt : une vignette centrée dans le conteneur (center). Les positions hors course sont rabattues sur les bords. Mesuré dans Chromium à 728 px de conteneur (page de 744 px) : course de 444 px et quatre arrêts seulement pour six vignettes — 0, 126, 318, 444 — ; seules la 3 et la 4 se centrent, les 1-2 et 5-6 ne sont visibles qu'aux butées, par paires. À 374 px (mobile) : course de 798 px, arrêts 0, 111, 303, 495, 687, 798, les vignettes 2 à 5 se centrent. À 900 px, la borne du conteneur sur un écran de bureau : course de 272 px, arrêts 0, 40, 232, 272 (scrollLeft = 100 se lit 40). Un cran de molette horizontale (100 unités) déplace de 117 px puis se pose à 126. Chromium snappe aussi les écritures programmatiques : scrollLeft = 100 se lit 126, 50 se lit 0 — impossible de laisser le carousel « entre deux ». Au doigt, chaque glissement de 150 px emmène à la vignette suivante (111 → 303 → 495 → 687) et un geste rapide de 300 px file jusqu'à la butée (798) : mandatory impose l'arrêt, pas la vitesse. Pas de scroll-behavior : un scrollTo() est instantané sauf à passer behavior: 'smooth' (≈ 230 ms pour 192 px dans Chromium).
Pourquoi une racine et une borne. L'indice est en position: absolute : il se cale sur l'ancêtre positionné le plus proche, et sans .sb-horiz ce serait la fenêtre (mesuré sur une version sans racine : indice à y = 579 dans une fenêtre de 600 px, le carousel finissant à y = 408). La racine le garde à 8 px du bas du carousel, où que le code soit collé, et elle porte le fond sombre sur lequel ses 30 % de blanc restent visibles. La borne, elle, tient à une propriété du défilement : un conteneur ne défile que si son contenu est plus long que lui. Sans max-width, un conteneur en width: 100% fait 1 264 px sur une page de 1 280 : la piste de 1 172 px y tient entière, scrollWidth = clientWidth, course nulle, rien ne défile (mesuré). Bornée à 900 px, la course est de 272 px sur cette même page ; dans une colonne déjà plus étroite (744 px de l'aperçu du catalogue, 374 px d'un mobile), la borne ne joue pas. Enfin, la molette verticale ne fait rien (0 px après trois crans de 120) : un conteneur horizontal n'écoute que son axe, et c'est voulu — détourner la molette verticale bloquerait le défilement de la page au passage du carousel. À la souris, il faut Maj + molette, une molette latérale, la barre de défilement ou le clavier ; pavé tactile et doigt défilent nativement.
Accessibilité
- prefers-reduced-motion absent du code. La seule animation est la pulsation d'opacité de l'indice (.3 ↔ .7, 2 s, infinie) ; mesuré sous
reduce,sdcHintPulsetourne toujours. Le snap lui-même est un défilement du navigateur, pas une animation, et sansscroll-behavior: smoothil n'y a rien d'autre à neutraliser. Ajoutez@media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none } }. - Clavier. Mesuré dans Chromium : Tab donne le focus au conteneur (les zones défilantes sans enfant focalisable sont focalisables depuis Chrome 130, et depuis longtemps dans Firefox), Flèche droite avance d'un arrêt (126, puis 318). Safari ne le fait pas : posez
tabindex="0",role="region"et unaria-label(« Destinations », « Examens ») sur.sb-horiz-content. Si les vignettes deviennent des liens, retirez letabindex: Tab fera défiler la vignette focalisée dans la vue. - Contraste. L'indice « Scrollez → » fait 10,4 px en blanc à 30 % sous une opacité qui pulse : de 1,22:1 à 1,83:1 sur
#0a0a0f(2,62:1 sans la pulsation) — jamais les 4,5:1 requis, et le texte est codé en français. La racine impose ce fond sombre : le rapport ne dépend pas de la page hôte. Soit vous en faites une vraie consigne lisible, soit vous le passez enaria-hidden="true"et mettez la consigne dans l'aria-labeldu conteneur. Les chiffres blancs des vignettes : 4,47:1 sur#6366f1mais 2,15:1 sur#f59e0b, 2,28:1 sur#22c55e, 2,43:1 sur#06b6d4— des espaces réservés à remplacer ; si du texte reste posé sur une image, ajoutez un voile sombre. - Lecteurs d'écran et affordance. Six
divsans sémantique de liste : passez enul/li(ourole="list") pour que le nombre d'éléments soit annoncé ; les vignettes hors champ restent dans l'arbre d'accessibilité, ce qui est correct.overflow: autolaisse la barre de défilement native, non stylée : sur les systèmes à barres classiques (Windows, macOS en « toujours afficher ») elle occupe le bas des 280 px du conteneur — c'est l'unique affordance des utilisateurs à souris, ne la masquez pas (scrollbar-width: none) sans ajouter des boutons.
Compatibilité navigateur
Requiert scroll-snap-type / scroll-snap-align (spécification 2018), la syntaxe à deux valeurs overflow: auto hidden et gap en contexte flex — c'est cette dernière qui fixe le seuil. Aucun JavaScript, aucune variable CSS, aucune animation-timeline : contrairement aux effets pilotés au défilement en CSS natif de cette catégorie, celui-ci fonctionne dans Firefox.
Sans JavaScript : rien à perdre, il n'y en a pas. Safari 13.1 à 14.0 ignore le gap en flex : les vignettes se touchent, le défilement et le snap fonctionnent. Safari 11 à 13.0 rejette overflow: auto hidden : le conteneur n'en est plus un, la piste de 1 172 px déborde et rien ne défile — écrivez overflow-x: auto; overflow-y: hidden. Sans snap (Firefox avant 68) : défilement horizontal libre, sans arrêt sur les vignettes.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="sb-horiz">
<div class="sb-horiz-content">
<div class="sb-horiz-track">
<div class="sb-horiz-item">1</div>
<div class="sb-horiz-item">2</div>
<div class="sb-horiz-item">3</div>
<div class="sb-horiz-item">4</div>
<div class="sb-horiz-item">5</div>
<div class="sb-horiz-item">6</div>
</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 |
|---|---|---|
Borne du conteneur (CSS — .sb-horiz-content) |
width: 100% ; max-width: 900px ; margin: 0 auto | La règle : ça défile tant que la piste (1 172 px pour six vignettes) est plus longue que le conteneur. Mesuré à 1 280 px de page : 272 px de course avec la borne, zéro sans. Adaptez-la à votre colonne, ou retirez-la si la colonne est déjà plus étroite que la piste ; élargissez la piste (plus de vignettes, ou plus larges) avant d'élargir le conteneur. |
Fond, couleur et police (CSS — .sb-horiz) |
#0a0a0f, #fff, sans-serif | La racine impose son fond : c'est ce qui garde l'indice visible sur une page blanche. Pour un carousel sur fond clair, changez ici le fond et recolorez l'indice (rgba(28,27,25,.55) dans la scène du showroom) ; la police n'affecte que les chiffres et l'indice. |
Taille des vignettes (CSS — .sb-horiz-item) |
180 × 140 px | Chaque vignette peut avoir sa propre largeur (classe ou style) : scroll-snap-align: center centre n'importe quelle largeur. Plus la vignette est étroite par rapport au conteneur, plus les voisines dépassent. |
scroll-snap-align (CSS — .sb-horiz-item) |
center | start aligne le bord gauche de la vignette sur celui du conteneur : la première devient atteignable à 0 et la dernière se cale à droite. Ajoutez alors scroll-padding-inline-start: 16px au conteneur pour respecter le retrait de la piste. |
scroll-snap-type (CSS — .sb-horiz-content) |
x mandatory | x proximity ne pose le défilement sur une vignette que lorsqu'on s'arrête près d'elle, et autorise les positions intermédiaires ; none rend le défilement libre. Avec mandatory, même un scrollLeft écrit en JavaScript est rabattu sur un arrêt (100 → 126 mesuré). |
Espacement et retrait de la piste (CSS — .sb-horiz-track) |
gap: 12px ; padding: 20px 16px | Le gap est la marge visible entre une vignette centrée et ses voisines ; le padding fixe la position des butées. Pour que la première et la dernière puissent se centrer, montez le padding horizontal à la moitié du conteneur moins la moitié d'une vignette (274 px à 728 px de large). |
Indice « Scrollez → » (HTML/CSS — .sdc-scroll-hint) |
.65rem, rgba(255,255,255,.3), pulsation 2 s | Ancré sur la racine .sb-horiz (position: relative) : laissez-le hors du conteneur défilant, sinon il part avec le contenu. Contraste 1,22 à 1,83:1 : à remplacer par une consigne lisible, ou à supprimer avec sa règle et l'animation sdcHintPulse. |
Fonds des vignettes (HTML — attributs style) |
6 dégradés à 135° chaînés (#6366f1 → … → #06b6d4) | Remplacez par background: url(…) center/cover ou une img en object-fit: cover dans la vignette ; les dégradés sont en ligne, ils disparaissent avec le HTML. |
FAQ
width: 100% sans borne fait 1 264 px, les six vignettes tiennent sur une ligne et scrollWidth = clientWidth — course nulle, mesuré. À 900 px, la course est de 272 px et le snap retrouve ses arrêts (0, 40, 232, 272). Vous pouvez monter la borne, ou la retirer, tant que votre piste reste plus longue que votre colonne : avec vos propres vignettes (dix photos de 240 px, par exemple), la piste dépasse 2 500 px et la borne devient inutile. L'aperçu du catalogue fait 744 px : il défile avec ou sans.overflow: auto hidden, scroll-snap-type: x mandatory) pour les deux premiers, mais pas le même contenu. Horizontal Snap : des diapositives en min-width: 100% alignées start, une seule visible à la fois, aucun aperçu des voisines. Card Snap Carousel : des cartes de 200 × 240 px avec une structure imposée (image de 100 px, titre, texte, étiquette), gap et padding sur le conteneur lui-même, et scroll-behavior: smooth. Sticky Horizontal ne défile pas du tout : sa piste glisse sur une animation CSS de 12 s (translateX de 0 à −400 %) dans un overflow: hidden. Horizontal Scroll est la version nue : 1,4 Ko de CSS, vignettes de 180 × 140 px sans structure interne, largeur libre par vignette, pas de scroll-behavior — à choisir quand l'utilisateur doit garder la main sur la vitesse et voir ce qui vient.scrollLeft à 0 — un conteneur horizontal n'écoute que l'axe horizontal (Maj + molette, molette latérale, pavé tactile, barre de défilement, ou Tab puis Flèche droite dans Chrome 130+ et Firefox). Pour des boutons, un scrollBy({ left: 192, behavior: 'smooth' }) suffit (largeur + gap) : avec mandatory, Chromium rabat la position sur l'arrêt le plus proche, donc même un décalage approximatif tombe sur une vignette (100 → 126 mesuré). Pour désactiver la flèche en butée, comparez scrollLeft à scrollWidth − clientWidth dans un écouteur scroll, ou scrollend si vous n'avez pas besoin de Safari avant la 26.