Card Snap Carousel
Six cartes de 200 × 240 px dans une piste de 280 px en scroll-snap-type: x mandatory, chacune calée au centre (scroll-snap-align: center) : les voisines dépassent des deux bords tant que la piste fait moins de 624 px. Aucun JavaScript — et scroll-behavior: smooth ne lisse que les défilements programmés, pas la molette.
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
.sn-carousel-container est la piste : width: 100%, height: 280px, overflow: auto hidden (défile en x, jamais en y), scroll-snap-type: x mandatory, scroll-behavior: smooth, flex avec align-items: center, gap: 12px et padding: 0 12px. Dedans, six .sn-carousel-card de 200 × 240 px (flex-shrink: 0, border-radius: 14px, fond blanc à 4 %, bordure de 1 px à 8 %), chacune portant scroll-snap-align: center. Une carte = un bandeau .sn-carousel-img de 100 px (numéro 01 à 06 en 1,5 rem / 800, dégradé à 135° propre à chaque nth-child : indigo → violet → fuchsia → rose → ambre → vert → cyan) et un corps .sn-carousel-body (padding 12 px, colonne flex, gap 4 px : h4 .82 rem, p .68 rem à 50 % de blanc, .sn-carousel-tag .65 rem violet poussé en bas par margin-top: auto). Piste totale : 12 + 6 × 200 + 5 × 12 + 12 = 1 284 px. Le HTML vendu se termine par .sdc-scroll-hint « Scrollez → », absolu à 8 px du bas de .demo-preview, dont l'opacité pulse de .3 à .7 toutes les 2 s (sdcHintPulse) — un fragment du catalogue partagé par 44 effets de la catégorie, pas une pièce du carousel. Zéro JavaScript.
L'arithmétique du snap : le centre de la carte n est à 112 + 212 × (n − 1) px du début de la piste ; la position d'arrêt vaut ce centre moins la moitié de la largeur visible, bornée entre 0 et 1 284 − largeur. Mesuré dans Chromium : piste de 524 px → six arrêts (0, 62, 274, 486, 698, 760), les cartes 2 à 5 centrées avec 150 px de chaque voisine visible, les cartes 1 et 6 collées au bord ; piste de 384 px (mobile) → 80 px de voisine ; piste de 728 px → 40 px seulement ; piste de 784 px → quatre arrêts pour six cartes (0, 144, 356, 500) : les cartes 1-2 partagent la position 0, les cartes 5-6 la fin de course, aucune des quatre n'est jamais centrée. Le « peek » symétrique n'existe que sous 624 px de piste — au-delà, (largeur − 200) / 2 − 12 dépasse 200 px et la voisine est entière.
scroll-behavior: smooth n'agit que sur les défilements programmés : scrollLeft = 274 mesuré 0 → 3 → 14 → 42 → 105 → 180 → 207 → 226 → 239 → 254 → 260 → 265 à 16 ms d'intervalle, soit ≈ 200 ms ; en auto, la même écriture est instantanée (274 au premier relevé). À la molette, les relevés sont identiques avec ou sans la propriété : un cran horizontal de 120 px porte la piste à 119 puis le snap la ramène à 62 en ≈ 180 ms — c'est le lissage du navigateur, pas le CSS. La molette verticale ne fait rien (trois crans, 0 px) : sans trackpad, c'est Maj + molette ou la barre. Un coup de 600 px saute de 0 à 500, la fin de course, sans s'arrêter aux cartes intermédiaires : pas de scroll-snap-stop: always. Au clavier dans Chromium, Tab focalise la piste et Flèche droite avance d'un arrêt (0 → 144). Aucune erreur console.
Accessibilité
- prefers-reduced-motion absent du code : mesuré à l'identique sous la préférence — le défilement programmé passe toujours par onze valeurs intermédiaires (1, 13, 41, 102…) et la pulsation
sdcHintPulsereste enrunning. Ajoutez@media (prefers-reduced-motion: reduce) { .sn-carousel-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }. Le snap lui-même n'est pas une animation : il reste, et c'est souhaitable. - Clavier : la piste n'a pas de
tabindex. Chromium la rend focalisable d'office (mesuré : Tab →.sn-carousel-container, Flèche droite = un arrêt), Firefox aussi, Safari non. Poseztabindex="0",role="region"et unaria-label, puis un style:focus-visible— le CSS n'en contient aucun. Les cartes ne sont pas interactives ; si vous y placez des liens, chaque carte hors champ reste tabulable et le navigateur la ramène dans la vue, snap compris. - Contrastes sur le fond de carte (#141419 une fois le 4 % de blanc posé sur #0a0a0f) : titre blanc 18,4:1,
pà 50 % de blanc 5,3:1, tag#a78bfa5,8:1 — mais en 10,9 px et 10,4 px, trop petit pour un texte utile : passez à 12-13 px. Numéros blancs sur les dégradés : 4,5:1 (carte 1) à 2,2:1 (carte 5, ambre → vert) et 2,3-2,4:1 (carte 6), sous le seuil de 3:1 même pour du texte de 24 px. L'indice « Scrollez → » oscille de 2,6:1 à 9,7:1 en 10,4 px, et il est écrit en français dans le HTML. - Souris sans trackpad : molette verticale = 0 px (mesuré). Le code n'a ni bouton précédent/suivant ni indicateur de position : ajoutez deux boutons qui font
scrollBy({ left: ±212 })— la propriétéscroll-behaviorlissera le déplacement. Rien ne dit au lecteur d'écran qu'il y a six cartes : un<ul>ourole="list"/role="listitem". La barre de défilement n'est pas masquée (aucune règlescrollbar-widthni::-webkit-scrollbar) : sous Windows elle prend ≈ 17 px dans les 280 px de la piste, sous les cartes. C'est le seul repère de position du composant — gardez-la ou remplacez-la par des points.
Compatibilité navigateur
CSS seul : scroll-snap-type: x mandatory / scroll-snap-align: center (syntaxe de 2018), overflow à deux valeurs, scroll-behavior: smooth, gap en flex et width: fit-content. Aucun animation-timeline : c'est du défilement natif, il fonctionne dans Firefox — contrairement aux animations pilotées au scroll en CSS, que Firefox ne rend dans aucune version. Zéro dépendance, zéro script.
Chrome 69-83 : snap et lissage complets mais gap ignoré en flex, les cartes se touchent. Firefox 68-93 : tout fonctionne sauf width: fit-content, la pastille de tag s'étire sur la largeur du corps. Safari 11-15.3 : snap natif, mais les défilements programmés sont instantanés (scroll-behavior ignoré) ; molette et doigt ne changent pas. Sans scroll-snap du tout : une rangée à défilement horizontal libre, sans arrêt sur les cartes.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="sn-carousel-container">
<div class="sn-carousel-card">
<div class="sn-carousel-img">01</div>
<div class="sn-carousel-body">
<h4>Titre</h4>
<p>Description</p>
<span class="sn-carousel-tag">Tag</span>
</div>
</div>
<!-- × 6 cartes -->
</div>
<span class="sdc-scroll-hint">Scrollez →</span>
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 |
|---|---|---|
| Largeur de carte (CSS — .sn-carousel-card width) | 200px | Avec gap: 12px, le pas entre deux arrêts vaut largeur + 12, et la part visible de chaque voisine vaut (piste − largeur) / 2 − 12. À 260 px sur une piste de 540 px : 128 px de voisine au lieu de 158. |
| scroll-snap-align (CSS — .sn-carousel-card) | center | start cale la carte active au bord gauche — ajoutez scroll-padding-left: 12px sur la piste pour respecter le padding — et rend la première carte atteignable, mais la dernière ne se cale plus. |
| scroll-snap-type (CSS — .sn-carousel-container) | x mandatory | x proximity ne cale que près d'un arrêt et laisse un défilement libre entre deux. Pour interdire de sauter des cartes (mesuré : un coup de 600 px va de 0 à la fin de course), ajoutez scroll-snap-stop: always sur .sn-carousel-card. |
| gap + padding (CSS — .sn-carousel-container) | 12px / 0 12px | Gardez-les égaux : la première et la dernière carte ont alors la même marge que l'espace entre deux cartes. Le padding fixe aussi la position de repos de la carte 1 (12 px du bord). |
| Hauteurs (CSS — .sn-carousel-container height / .sn-carousel-card height) | 280px / 240px | 20 px libres au-dessus et sous les cartes ; c'est là que se loge la barre de défilement quand le système l'affiche. À 260 px de piste, elle mord dans les cartes. |
| scroll-behavior (CSS — .sn-carousel-container) | smooth | Ne concerne que scrollLeft, scrollTo, scrollBy et les ancres (≈ 200 ms mesurés) ; la molette et le doigt n'en dépendent pas. À passer en auto sous prefers-reduced-motion. |
| Dégradés des bandeaux (CSS — .sn-carousel-card:nth-child(1…6) .sn-carousel-img) | 6 dégradés à 135°, indigo → cyan | Six règles pour six cartes : une septième carte n'a pas de fond. Écrivez nth-child(6n+1)… pour cycler, ou une seule couleur pour tous les bandeaux. |
| Indice « Scrollez → » (HTML — .sdc-scroll-hint) | bottom: 8px, opacité .3 ↔ .7 en 2 s | Positionné contre l'ancêtre positionné le plus proche (.demo-preview dans le code vendu). Traduisez-le, remplacez-le par des boutons ou supprimez-le : il est décoratif et son contraste tombe à 2,6:1 au creux de la pulsation. |
FAQ
scrollLeft négatif, centrer la carte 6 dépasserait la fin de course. Mesuré sur une piste de 784 px : les cartes 1 et 2 partagent l'arrêt 0, les cartes 5 et 6 l'arrêt 500 — quatre positions pour six cartes. Le correctif est un padding qui crée l'espace manquant : .sn-carousel-container { padding: 0 calc(50% - 100px) } (la moitié de la piste moins la moitié d'une carte). Chaque carte devient alors centrable, la première et la dernière comprises, et le nombre d'arrêts vaut le nombre de cartes quelle que soit la largeur.sn-, même piste (overflow: auto hidden, x mandatory, smooth, 280 px), mais fx-0594 fait des sections à min-width: 100% calées en start : un panneau par arrêt, rien des voisins, toujours autant d'arrêts que de sections. fx-0595 fixe des cartes de 200 px calées au centre : les voisines dépassent, et le nombre d'arrêts dépend de la largeur (six à 524 px, quatre à 784 px). Prenez fx-0594 pour une séquence à lire un message à la fois, fx-0595 pour parcourir et comparer plusieurs éléments courts.track.addEventListener('wheel', e => { if (!e.deltaY) return; e.preventDefault(); track.scrollBy({ left: e.deltaY }); }, { passive: false }) — scroll-behavior: smooth lisse le déplacement et le snap mandatory termine sur une carte. Le prix : la page ne défile plus verticalement tant que le pointeur est sur la piste, y compris en fin de course ; testez scrollLeft contre 0 et scrollWidth − clientWidth avant d'annuler l'événement. Deux boutons scrollBy({ left: ±212 }) restent la solution la plus lisible.