Maîtrisez CSS Scroll Snap pour créer des défilements fluides et contrôlés : guide complet avec exemples pratiques et cas d'usage.
Introduction — les limites du scroll natif
Le défilement est l'interaction la plus fondamentale du web. Pourtant, pendant longtemps, dès qu'un développeur voulait créer un carousel, une galerie d'images ou une navigation page par page, la seule solution était d'importer une bibliothèque JavaScript volumineuse : Swiper, Slick, Flickity, Owl Carousel... Chacune avec ses dépendances, ses configurations, ses conflits potentiels et son impact sur les performances.
Le problème du scroll natif est simple : sans contrainte, l'utilisateur peut s'arrêter n'importe où dans le conteneur défilant. On peut se retrouver avec un slide à moitié visible, un élément coupé en deux, une expérience visuellement cassée. Les bibliothèques JavaScript résolvaient ce problème en interceptant les événements de défilement et en recalculant la position — au prix d'une latence perceptible et d'une consommation CPU non négligeable.
CSS Scroll Snap est la réponse native du W3C à ce problème. Introduit en 2016 et aujourd'hui supporté par tous les navigateurs modernes, ce module CSS permet de définir des points d'accrochage (snap points) sur un conteneur défilant. Le navigateur gère lui-même le calcul et l'animation de l'alignement, avec une fluidité que JavaScript ne peut pas égaler — car l'animation tourne directement sur le thread de composition, sans bloquer le thread principal.
- Les trois propriétés CSS principales :
scroll-snap-type,scroll-snap-align,scroll-snap-stop - L'ajustement fin avec
scroll-paddingetscroll-margin - La création d'un carousel, d'une galerie et d'une navigation plein-écran
- La combinaison avec
overscroll-behaviorpour un contrôle avancé - Les bonnes pratiques d'accessibilité et de performance
Support navigateurs en 2025
| Propriété | Chrome | Firefox | Safari | Edge | Couverture globale |
|---|---|---|---|---|---|
scroll-snap-type |
69+ | 68+ | 11+ | 79+ | ~97% |
scroll-snap-align |
69+ | 68+ | 11+ | 79+ | ~97% |
scroll-snap-stop |
75+ | 103+ | 15+ | 79+ | ~95% |
scroll-padding |
69+ | 68+ | 11+ | 79+ | ~97% |
Les trois propriétés fondamentales
CSS Scroll Snap repose sur un modèle très simple : un conteneur (le scroll container) et des enfants (les snap items). Deux niveaux, trois propriétés principales à connaître.
Vue d'ensemble du modèle
/* ===================================================
MODÈLE CSS SCROLL SNAP — Architecture de base
=================================================== */
/* 1. Sur le CONTENEUR — définit le comportement de snap */
.scroll-container {
/* Axe de défilement + mode d'accrochage */
scroll-snap-type: x mandatory;
/* Rendre le conteneur scrollable sur l'axe x */
overflow-x: scroll;
/* Masquer la barre de défilement (cosmétique) */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge ancien */
}
/* Masquer la scrollbar webkit (Chrome, Safari) */
.scroll-container::-webkit-scrollbar {
display: none;
}
/* 2. Sur les ENFANTS — définit le point d'accrochage */
.scroll-item {
/* Où l'élément s'aligne dans le conteneur */
scroll-snap-align: start;
/* Empêcher l'utilisateur de sauter cet élément */
scroll-snap-stop: always;
}
C'est tout ce qu'il faut pour un carousel de base fonctionnel. Les sections suivantes détaillent chaque propriété et ses variantes.
scroll-snap-type se place
toujours sur le conteneur défilant (celui qui a overflow: scroll
ou overflow: auto). scroll-snap-align se place
toujours sur les enfants directs de ce conteneur.
scroll-snap-type en détail
scroll-snap-type est la propriété maîtresse. Elle s'applique
sur le conteneur et prend deux paramètres : l'axe de défilement
et la rigueur de l'accrochage.
Syntaxe complète
/* Syntaxe : scroll-snap-type: */
/* Axe horizontal uniquement — carousel gauche/droite */
scroll-snap-type: x mandatory;
/* Axe vertical uniquement — navigation haut/bas, stories */
scroll-snap-type: y mandatory;
/* Les deux axes — galerie 2D (rare) */
scroll-snap-type: both mandatory;
/* Axe de bloc (vertical en écriture occidentale) */
scroll-snap-type: block mandatory;
/* Axe inline (horizontal en écriture occidentale) */
scroll-snap-type: inline mandatory;
/* Désactivé */
scroll-snap-type: none;
mandatory vs proximity — le choix crucial
/* ================================
mandatory : accrochage FORCÉ
================================
Le navigateur DOIT toujours finir sur un snap point.
Même si l'utilisateur lâche le scroll au milieu,
le conteneur revient automatiquement au point le plus proche.
✅ Idéal pour : carousels, sliders, sections plein-écran
⚠️ Risque : si les snap items sont trop grands (> viewport),
certains contenus peuvent devenir inaccessibles */
.carousel {
scroll-snap-type: x mandatory;
}
/* ================================
proximity : accrochage CONDITIONNEL
================================
Le navigateur accroche SEULEMENT si l'élément est
"suffisamment proche" d'un snap point (heuristique du navigateur).
L'utilisateur peut s'arrêter entre deux points.
✅ Idéal pour : listes longues, défilement partiellement contrôlé
✅ Plus permissif — évite le blocage de contenu */
.feed {
scroll-snap-type: y proximity;
}
mandatory, l'utilisateur ne peut pas
s'arrêter entre deux articles. Avec proximity, il peut parcourir
librement et le snap ne s'active que s'il s'approche d'une frontière. Pour les
listes longues, proximity est souvent plus confortable.
Exemple comparatif — mandatory vs proximity
<!-- Structure HTML identique pour les deux exemples -->
<div class="demo-container demo-mandatory">
<div class="demo-item">Slide 1</div>
<div class="demo-item">Slide 2</div>
<div class="demo-item">Slide 3</div>
</div>
<div class="demo-container demo-proximity">
<div class="demo-item">Item 1</div>
<div class="demo-item">Item 2</div>
<div class="demo-item">Item 3</div>
</div>
/* CSS — comparaison mandatory vs proximity */
.demo-container {
display: flex;
overflow-x: scroll;
gap: 1rem;
padding: 1rem;
width: 100%;
scrollbar-width: none;
}
.demo-container::-webkit-scrollbar { display: none; }
.demo-item {
flex: 0 0 80%; /* 80% de la largeur visible = effet carousel */
height: 200px;
border-radius: 12px;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
scroll-snap-align: start; /* point d'accrochage sur tous les items */
}
/* Mandatory : toujours aligné */
.demo-mandatory {
scroll-snap-type: x mandatory;
}
/* Proximity : aligné seulement si proche */
.demo-proximity {
scroll-snap-type: x proximity;
}
scroll-snap-align : start, center, end
scroll-snap-align définit où le bord de l'enfant
s'aligne par rapport au conteneur. Cette propriété se place sur chaque
élément enfant et prend une ou deux valeurs.
Les trois valeurs d'alignement
/* scroll-snap-align:
(une seule valeur = appliquée aux deux axes) */
/* ✅ start — le bord de départ de l'enfant s'aligne
sur le bord de départ du conteneur.
En scroll horizontal LTR : bord gauche de l'item = bord gauche du conteneur.
✅ Idéal pour : carousel classique, navigation de contenu */
.item {
scroll-snap-align: start;
}
/* ✅ center — le centre de l'enfant s'aligne sur le centre du conteneur.
✅ Idéal pour : galeries d'images, cartes avec effet "focus" */
.item {
scroll-snap-align: center;
}
/* ✅ end — le bord de fin de l'enfant s'aligne
sur le bord de fin du conteneur.
En scroll horizontal LTR : bord droit de l'item = bord droit du conteneur.
✅ Idéal pour : listes inversées, interfaces chat */
.item {
scroll-snap-align: end;
}
/* ✅ none — désactive le snap pour cet élément spécifique */
.item-special {
scroll-snap-align: none;
}
Deux valeurs pour les axes indépendants
/* Syntaxe à deux valeurs :
block = axe vertical (en écriture horizontale)
inline = axe horizontal (en écriture horizontale) */
/* Alignement vertical sur "start", horizontal sur "center" */
.grid-item {
scroll-snap-align: start center;
}
/* Les deux axes sur "center" — galerie 2D */
.gallery-item {
scroll-snap-align: center center;
/* Équivalent à : scroll-snap-align: center; */
}
Exemple visuel — impact de l'alignement sur un carousel
/* ================================================
CAROUSEL avec scroll-snap-align: start
L'utilisateur voit chaque item depuis son début
================================================ */
.carousel-start {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
gap: 16px;
padding: 0 24px; /* espace visible avant le premier item */
scrollbar-width: none;
}
.carousel-start::-webkit-scrollbar { display: none; }
.carousel-start .slide {
flex: 0 0 calc(100% - 48px); /* pleine largeur moins les paddings */
height: 280px;
border-radius: 16px;
scroll-snap-align: start; /* bord gauche du slide = bord gauche visible */
background: linear-gradient(135deg, #667eea, #764ba2);
}
/* ================================================
CAROUSEL avec scroll-snap-align: center
L'item actif est toujours centré — effet "focus"
Les slides voisins sont partiellement visibles
================================================ */
.carousel-center {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
gap: 16px;
padding: 0 10%; /* affiche les slides adjacents */
scrollbar-width: none;
}
.carousel-center::-webkit-scrollbar { display: none; }
.carousel-center .slide {
flex: 0 0 80%; /* 80% = visible, 10% de chaque côté = voisins */
height: 280px;
border-radius: 16px;
scroll-snap-align: center; /* centre du slide = centre du conteneur */
background: linear-gradient(135deg, #f093fb, #f5576c);
transition: transform 0.3s ease, opacity 0.3s ease;
}
scroll-snap-stop : forcer l'arrêt
Par défaut, un geste de défilement rapide peut faire "sauter" plusieurs
snap points d'un coup. L'utilisateur fait un flick sur mobile et se retrouve
trois slides plus loin sans voir les intermédiaires. scroll-snap-stop
résout ce comportement.
normal vs always
/* scroll-snap-stop: normal (valeur par défaut)
Le défilement peut passer en survol les snap points
si l'élan est suffisant. Un flick rapide → plusieurs slides sautés.
✅ Comportement fluide pour les listes longues */
.slide {
scroll-snap-stop: normal;
}
/* scroll-snap-stop: always
Le défilement s'ARRÊTE obligatoirement sur chaque snap point,
même si l'utilisateur a eu un geste très rapide.
Chaque slide est visible au moins un instant.
✅ Idéal pour : tutoriels étape par étape, onboarding,
galeries où chaque image compte */
.slide {
scroll-snap-stop: always;
}
Exemple pratique — carousel d'onboarding
<!-- Onboarding en 3 étapes : l'utilisateur DOIT voir chaque étape -->
<div class="onboarding-container">
<div class="onboarding-step">
<h2>Étape 1 — Bienvenue</h2>
<p>Découvrez les fonctionnalités principales.</p>
</div>
<div class="onboarding-step">
<h2>Étape 2 — Configurez votre profil</h2>
<p>Personnalisez votre expérience.</p>
</div>
<div class="onboarding-step">
<h2>Étape 3 — Vous êtes prêt !</h2>
<p>Commencez à utiliser l'application.</p>
</div>
</div>
/* CSS — carousel d'onboarding avec arrêt forcé */
.onboarding-container {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scrollbar-width: none;
width: 100%;
}
.onboarding-container::-webkit-scrollbar { display: none; }
.onboarding-step {
flex: 0 0 100%; /* chaque étape = 100% de la largeur visible */
min-height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
text-align: center;
/* Les deux propriétés clés */
scroll-snap-align: start; /* alignement sur le bord gauche */
scroll-snap-stop: always; /* forcer l'arrêt — aucun skip possible */
}
.onboarding-step:nth-child(1) { background-color: #e8f4fd; }
.onboarding-step:nth-child(2) { background-color: #fef9e7; }
.onboarding-step:nth-child(3) { background-color: #e8f8f0; }
scroll-snap-stop: always
est traité nativement par le navigateur sans JavaScript. Il n'y a pas d'overhead
de calcul — le moteur de rendu gère l'élan et l'arrêt directement au niveau
du thread de composition.
scroll-padding et scroll-margin
Ces deux propriétés permettent d'affiner la position d'accrochage. Elles sont particulièrement utiles quand un header fixe masque le haut du contenu, ou quand on veut un espace visuel entre le bord du conteneur et l'élément accroché.
scroll-padding — décalage côté conteneur
/* scroll-padding s'applique sur le CONTENEUR.
Il définit une zone d'exclusion à l'intérieur du conteneur —
les snap points ne peuvent pas s'y loger.
Cas d'usage classique : header fixe qui masque le haut des sections */
/* Sans scroll-padding : la section s'accroche derrière le header */
html {
scroll-snap-type: y mandatory;
overflow-y: scroll;
}
/* Avec scroll-padding : décale le snap point vers le bas */
html {
scroll-snap-type: y mandatory;
overflow-y: scroll;
scroll-padding-top: 80px; /* hauteur du header fixe */
}
/* Variantes par côté */
.container {
scroll-padding: 20px; /* 4 côtés identiques */
scroll-padding: 20px 0; /* vertical | horizontal */
scroll-padding: 60px 0 0 0; /* top right bottom left */
scroll-padding-top: 60px; /* un côté spécifique */
scroll-padding-inline-start: 24px; /* logique CSS (début de ligne) */
}
scroll-margin — décalage côté enfant
/* scroll-margin s'applique sur l'ENFANT (le snap item).
Il ajoute un décalage autour de l'élément pour le calcul de l'accrochage.
Analogue à margin, mais affecte uniquement la position de snap. */
/* Laisser 16px d'espace avant chaque section lors de l'accrochage */
.section-page {
scroll-snap-align: start;
scroll-margin-top: 16px; /* espace au-dessus lors du snap */
}
/* Carousel horizontal : espace de 24px avant chaque slide */
.carousel-slide {
scroll-snap-align: start;
scroll-margin-left: 24px; /* espace à gauche lors du snap */
}
/* Syntaxe logique CSS (recommandée pour l'internationalisation) */
.slide {
scroll-snap-align: start;
scroll-margin-inline-start: 24px; /* équivalent à scroll-margin-left en LTR */
}
Combinaison scroll-padding + scroll-margin
/* Exemple complet : page avec header fixe de 64px + carousel latéral */
/* 1. Sur le conteneur de page entière */
.page-wrapper {
overflow-y: scroll;
scroll-snap-type: y mandatory;
scroll-padding-top: 64px; /* décale le snap sous le header fixe */
}
/* 2. Sur chaque section plein-écran */
.full-section {
height: 100vh;
scroll-snap-align: start;
/* Le snap s'accroche à 64px du haut → contenu non masqué par le header */
}
/* 3. Carousel horizontal intégré dans une section */
.horizontal-carousel {
overflow-x: scroll;
scroll-snap-type: x mandatory;
display: flex;
scroll-padding-inline: 24px; /* espace visible de chaque côté */
}
.horizontal-carousel .card {
flex: 0 0 300px;
scroll-snap-align: start;
scroll-margin-inline-start: 0; /* pas de décalage supplémentaire ici */
}
Cas d'usage réels : carousel, galerie, stories
CSS Scroll Snap brille dans plusieurs patterns d'interface courants. Voici les implémentations complètes des cas les plus fréquents.
Cas 1 — Carousel d'images horizontal (Bootstrap 5)
<!-- Carousel CSS pur — pas de JavaScript requis pour le défilement -->
<div class="css-carousel" role="region" aria-label="Galerie de projets">
<div class="css-carousel__track">
<figure class="css-carousel__slide">
<img src="projet-1.webp" alt="Projet e-commerce React" loading="lazy" width="400" height="250">
<figcaption class="css-carousel__caption">E-commerce React</figcaption>
</figure>
<figure class="css-carousel__slide">
<img src="projet-2.webp" alt="Dashboard Angular" loading="lazy" width="400" height="250">
<figcaption class="css-carousel__caption">Dashboard Angular</figcaption>
</figure>
<figure class="css-carousel__slide">
<img src="projet-3.webp" alt="API NestJS" loading="lazy" width="400" height="250">
<figcaption class="css-carousel__caption">API NestJS</figcaption>
</figure>
</div>
</div>
/* CSS — carousel d'images avec Bootstrap 5 */
.css-carousel {
width: 100%;
overflow: hidden; /* masque le débordement du track */
border-radius: 16px;
}
.css-carousel__track {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scrollbar-width: none;
-webkit-overflow-scrolling: touch; /* momentum scroll iOS */
gap: 0; /* pas d'espace entre les slides */
}
.css-carousel__track::-webkit-scrollbar { display: none; }
.css-carousel__slide {
flex: 0 0 100%; /* pleine largeur */
scroll-snap-align: start;
scroll-snap-stop: always;
margin: 0;
position: relative;
}
.css-carousel__slide img {
width: 100%;
height: 280px;
object-fit: cover;
display: block;
}
.css-carousel__caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
color: #fff;
padding: 1.5rem 1rem 1rem;
margin: 0;
font-weight: 600;
}
Cas 2 — Navigation de page en page (sections plein-écran)
<!-- Landing page avec sections plein-écran -->
<main class="fullpage-scroll">
<section id="hero" class="fullpage-section bg-primary text-white">
<div class="container h-100 d-flex align-items-center">
<h1>Bienvenue sur notre plateforme</h1>
</div>
</section>
<section id="features" class="fullpage-section bg-light">
<div class="container h-100 d-flex align-items-center">
<h2>Nos fonctionnalités</h2>
</div>
</section>
<section id="pricing" class="fullpage-section bg-white">
<div class="container h-100 d-flex align-items-center">
<h2>Tarifs</h2>
</div>
</section>
<section id="contact" class="fullpage-section bg-dark text-white">
<div class="container h-100 d-flex align-items-center">
<h2>Contactez-nous</h2>
</div>
</section>
</main>
/* CSS — navigation page par page avec scroll vertical */
.fullpage-scroll {
height: 100vh; /* conteneur = hauteur du viewport */
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
}
.fullpage-scroll::-webkit-scrollbar { display: none; }
.fullpage-section {
height: 100vh; /* chaque section = 100% de la hauteur visible */
scroll-snap-align: start;
/* Le contenu est centré verticalement via Bootstrap (h-100 + align-items-center) */
}
/* Navigation fixe compatible avec scroll-snap */
.nav-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 70px;
z-index: 1000;
}
/* Décaler les sections pour ne pas être masquées par le header */
.fullpage-scroll {
scroll-padding-top: 70px;
}
Cas 3 — Stories mobile (scroll vertical, plein-écran)
<!-- Interface "stories" type Instagram/TikTok -->
<div class="stories-feed" role="region" aria-label="Stories">
<article class="story" aria-label="Story 1">
<img src="story-1.webp" alt="Story de Jean - Vue sur Paris" class="story__media">
<div class="story__overlay">
<span class="story__author">@jean_dupont</span>
<p class="story__text">Belle journée à Paris !</p>
</div>
</article>
<article class="story" aria-label="Story 2">
<video src="story-2.mp4" autoplay muted loop class="story__media"></video>
<div class="story__overlay">
<span class="story__author">@marie_code</span>
<p class="story__text">Nouveau projet lancé 🚀</p>
</div>
</article>
</div>
/* CSS — stories plein-écran avec snap vertical */
.stories-feed {
height: 100svh; /* svh = Small Viewport Height (mobile-safe) */
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
background: #000;
}
.stories-feed::-webkit-scrollbar { display: none; }
.story {
height: 100svh; /* chaque story = plein écran */
position: relative;
scroll-snap-align: start;
scroll-snap-stop: always; /* interdit de sauter une story */
overflow: hidden;
}
.story__media {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.story__overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2rem 1.5rem;
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 100%);
color: #fff;
}
.story__author {
display: block;
font-weight: 700;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.story__text {
font-size: 0.95rem;
margin: 0;
line-height: 1.5;
}
Scroll Snap vs JavaScript
Avant l'adoption large de CSS Scroll Snap, les équipes utilisaient du JavaScript pour intercepter et corriger la position de défilement. Comprenons les compromis entre les deux approches.
Approche JavaScript — avant CSS Scroll Snap
/**
* Implémentation JavaScript "maison" d'un carousel snap
* — Montre la complexité évitée par CSS Scroll Snap
*/
class ManualCarousel {
constructor(container) {
this.container = container;
this.slides = [...container.querySelectorAll('.slide')];
this.current = 0;
this._bindEvents();
}
_bindEvents() {
// Écouter la fin du défilement (événement "scrollend" ou timer)
this.container.addEventListener('scroll', this._onScroll.bind(this));
}
_onScroll() {
// Annuler le timer précédent (debounce)
clearTimeout(this._scrollTimer);
// Attendre la fin du défilement (150ms de silence)
this._scrollTimer = setTimeout(() => {
this._snapToNearest();
}, 150);
}
_snapToNearest() {
const scrollLeft = this.container.scrollLeft;
const slideWidth = this.slides[0].offsetWidth;
// Calculer le slide le plus proche
const nearest = Math.round(scrollLeft / slideWidth);
this.current = Math.max(0, Math.min(nearest, this.slides.length - 1));
// Animer vers la position (behaviour: smooth)
this.container.scrollTo({
left: this.current * slideWidth,
behavior: 'smooth'
});
}
}
/* Problèmes de cette approche :
- Latence du debounce (150ms de délai ressenti)
- Calculs sur le thread principal (bloque le rendu)
- Gestes tactiles mal gérés (inertie iOS/Android)
- Code à maintenir et à tester */
Comparaison des performances
| Critère | CSS Scroll Snap | JavaScript "maison" | Bibliothèque (Swiper) |
|---|---|---|---|
| Thread | ✅ Composition (60fps) | ⚠️ Main thread | ⚠️ Main thread |
| Latence tactile | ✅ Nulle (natif) | ❌ 150ms+ debounce | ⚠️ Variable |
| Poids bundle | ✅ 0 KB JS | ⚠️ ~1-2 KB | ❌ ~30-150 KB |
| Accessibilité | ✅ Native | ⚠️ À implémenter | ⚠️ Variable |
| Fonctionnalités avancées | ❌ Limitées | ✅ Illimitées | ✅ Complètes |
| Maintenance | ✅ Zéro | ❌ Élevée | ⚠️ Dépendance externe |
Hybride — CSS Snap + JS minimal pour les contrôles
/**
* Approche recommandée :
* CSS Scroll Snap gère le snap (performances)
* JavaScript minimal gère uniquement les boutons prev/next
* et les indicateurs de position (dots)
*/
const carousel = document.querySelector('.css-carousel__track');
const slides = carousel.querySelectorAll('.css-carousel__slide');
const dots = document.querySelectorAll('.carousel-dot');
// Boutons prev/next — JavaScript scroll programmatique
document.querySelector('.btn-prev').addEventListener('click', () => {
// scrollBy utilise le snap automatiquement !
carousel.scrollBy({ left: -carousel.offsetWidth, behavior: 'smooth' });
});
document.querySelector('.btn-next').addEventListener('click', () => {
carousel.scrollBy({ left: carousel.offsetWidth, behavior: 'smooth' });
});
// Mise à jour des dots — écouter scrollend (Chrome 114+)
// ou IntersectionObserver pour plus de compatibilité
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
const index = [...slides].indexOf(entry.target);
dots.forEach((dot, i) => dot.classList.toggle('active', i === index));
}
});
}, {
root: carousel,
threshold: 0.5 /* slide considéré actif si visible à 50% */
});
slides.forEach(slide => observer.observe(slide));
Combinaison avec overscroll-behavior
overscroll-behavior est une propriété CSS complémentaire
qui contrôle ce qui se passe quand l'utilisateur atteint les limites
d'un conteneur défilant. Elle est particulièrement utile combinée
avec CSS Scroll Snap.
Les valeurs d'overscroll-behavior
/* overscroll-behavior : comportement en fin de scroll */
/* auto (défaut) : scroll chaining — le scroll se propage
au conteneur parent quand l'enfant atteint sa limite.
Comportement standard du navigateur. */
.container {
overscroll-behavior: auto;
}
/* contain : bloque la propagation au parent.
Le scroll reste "contenu" dans le conteneur courant.
✅ Idéal pour les modales, sidebars, carousels imbriqués */
.modal-body {
overscroll-behavior: contain;
}
/* none : désactive le bounce/glow natif ET bloque la propagation.
✅ Utile pour les PWA plein-écran, les jeux, le fullpage scroll */
.fullpage-scroll {
overscroll-behavior: none;
}
/* Axes indépendants */
.carousel {
overscroll-behavior-x: contain; /* horizontal contenu */
overscroll-behavior-y: auto; /* vertical propagé normalement */
}
Exemple — carousel sans scroll chaining
/* Problème sans overscroll-behavior :
L'utilisateur fait défiler le carousel jusqu'à la fin,
puis continue → le scroll se propage à la PAGE entière.
Mauvaise UX : on navigue accidentellement sur la page. */
/* Solution : contenir le scroll dans le carousel */
.carousel-track {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scrollbar-width: none;
/* Empêche le scroll de "déborder" sur la page */
overscroll-behavior-x: contain;
}
.carousel-track::-webkit-scrollbar { display: none; }
.carousel-slide {
flex: 0 0 100%;
scroll-snap-align: start;
scroll-snap-stop: always;
}
Fullpage scroll sans effet bounce (iOS)
/* Sur iOS, le "bounce" (rebond) en fin de scroll peut
donner l'impression que la page se casse en fullpage mode.
overscroll-behavior: none supprime ce rebond. */
.fullpage-container {
height: 100svh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
/* Supprimer le bounce iOS et le pull-to-refresh Android */
overscroll-behavior-y: none;
}
/* Important : appliquer aussi sur body pour iOS */
body {
overscroll-behavior: none;
}
Responsive et adaptation mobile
CSS Scroll Snap est particulièrement efficace sur mobile, où les gestes tactiles natifs sont déjà bien gérés par le navigateur. L'enjeu est d'adapter le comportement selon la taille d'écran.
Désactiver le snap sur desktop
/* Pattern courant : liste scrollable avec snap sur mobile,
grille fixe sur desktop */
/* Mobile-first : snap activé */
.product-list {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
gap: 16px;
padding: 0 16px;
scrollbar-width: none;
}
.product-list::-webkit-scrollbar { display: none; }
.product-card {
flex: 0 0 280px;
scroll-snap-align: start;
}
/* Desktop (≥768px) : grille standard, pas de snap */
@media (min-width: 768px) {
.product-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
overflow-x: visible; /* annule le scroll horizontal */
scroll-snap-type: none; /* désactive le snap */
padding: 0;
}
.product-card {
flex: none; /* annule le flex sizing mobile */
scroll-snap-align: none;
}
}
Adapter la taille des slides selon l'écran
/* Carousel responsive : 1 slide sur mobile, 2 sur tablette, 3 sur desktop */
.adaptive-carousel {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
gap: 16px;
padding: 0 16px;
scrollbar-width: none;
}
.adaptive-carousel::-webkit-scrollbar { display: none; }
.adaptive-slide {
scroll-snap-align: start;
flex-shrink: 0;
/* Mobile : 1 slide = pleine largeur moins les paddings */
flex-basis: calc(100% - 32px);
}
/* Tablette : 2 slides visibles */
@media (min-width: 576px) {
.adaptive-slide {
flex-basis: calc(50% - 16px); /* (100% / 2) - gap/2 */
}
}
/* Desktop : 3 slides visibles */
@media (min-width: 992px) {
.adaptive-slide {
flex-basis: calc(33.333% - 12px); /* (100% / 3) - gap*2/3 */
}
}
Unités viewport sûres pour mobile
/* Sur mobile, 100vh inclut la barre d'adresse → overlap.
Les nouvelles unités CSS résolvent ce problème : */
/* svh — Small Viewport Height : exclut la barre d'adresse rétractable
✅ Recommandé pour les fullpage layouts mobiles */
.fullpage-section {
height: 100svh;
}
/* dvh — Dynamic Viewport Height : s'adapte en temps réel
(peut causer des saccades au scroll si la barre se rétracte) */
.section-dynamic {
height: 100dvh;
}
/* lvh — Large Viewport Height : inclut la barre d'adresse
(comme 100vh classique) */
.section-large {
height: 100lvh;
}
/* Fallback pour les navigateurs anciens (iOS < 15.4) */
.fullpage-section {
height: 100vh; /* fallback */
height: 100svh; /* override si supporté */
}
Orientation landscape — adapter le scroll
/* En landscape sur mobile, 100vh est très court.
Adapter le comportement selon l'orientation */
@media (orientation: landscape) and (max-height: 500px) {
/* Désactiver le fullpage snap en landscape mobile */
.fullpage-scroll {
scroll-snap-type: none;
overflow-y: auto;
}
.fullpage-section {
height: auto;
min-height: 100svh;
padding: 2rem;
}
}
Bonnes pratiques et pièges à éviter
1. Toujours respecter prefers-reduced-motion
/* CSS Scroll Snap utilise des animations de scroll fluides.
Les utilisateurs sensibles aux animations peuvent les désactiver.
Toujours respecter cette préférence ! */
@media (prefers-reduced-motion: reduce) {
/* Désactiver le smooth scroll sur tout le document */
html {
scroll-behavior: auto !important;
}
/* Optionnel : désactiver le snap lui-même pour un défilement libre */
.fullpage-scroll,
.carousel-track {
scroll-snap-type: none !important;
}
}
/* Note : le scroll snap lui-même n'est pas une "animation" au sens CSS,
mais le scrollTo({ behavior: 'smooth' }) JS l'est.
Vérifier avant d'appeler scrollTo avec smooth : */
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
container.scrollTo({
left: targetX,
behavior: prefersReduced ? 'instant' : 'smooth'
});
2. Le piège du contenu inaccessible avec mandatory
/* ❌ PROBLÈME : scroll-snap-type: mandatory + éléments plus hauts que le viewport
→ certaines parties deviennent inaccessibles (le snap saute par-dessus) */
/* Exemple problématique */
.container {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory; /* ⚠️ dangereux si les sections sont variables */
}
.section-variable {
min-height: 100vh;
/* Si la section fait 150vh → le bas est inaccessible avec mandatory ! */
scroll-snap-align: start;
}
/* ✅ SOLUTION : utiliser proximity pour les contenus à hauteur variable */
.container-safe {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y proximity; /* tolère les grandes sections */
}
/* OU : garantir que chaque section = exactement 100vh */
.section-fixed {
height: 100vh; /* jamais "min-height" avec mandatory */
overflow-y: auto; /* scroll interne si contenu dépasse */
scroll-snap-align: start;
}
3. Ne pas confondre le conteneur scroll et le conteneur de layout
/* ❌ Erreur fréquente : mettre scroll-snap-type sur un conteneur
sans overflow: scroll/auto */
.wrapper {
scroll-snap-type: x mandatory; /* inefficace ! wrapper n'est pas scrollable */
}
.wrapper .item {
scroll-snap-align: start;
}
/* ✅ Correct : le conteneur scrollable = celui avec overflow */
.wrapper {
/* pas de scroll-snap-type ici */
}
.wrapper .scroll-area {
overflow-x: scroll;
scroll-snap-type: x mandatory; /* sur LE conteneur scrollable */
display: flex;
}
.wrapper .scroll-area .item {
scroll-snap-align: start; /* sur les ENFANTS DIRECTS du scrollable */
}
4. Accessibilité — navigation clavier et focus
/* CSS Scroll Snap interagit naturellement avec la navigation clavier :
Tab → focus sur l'élément → le scroll s'y ajuste automatiquement.
Vérifier que les éléments focusables sont bien snap-alignés. */
/* Rendre les slides focusables pour la navigation clavier */
.slide {
scroll-snap-align: start;
scroll-snap-stop: always;
/* Permettre le focus */
tabindex: 0; /* en HTML : tabindex="0" */
outline: none;
}
/* Indiquer le focus visuellement (ne jamais supprimer sans remplacer) */
.slide:focus-visible {
outline: 3px solid #0d6efd;
outline-offset: -3px;
}
/* En HTML, toujours ajouter les attributs ARIA sur le conteneur */
/* <div class="carousel" role="region" aria-label="Galerie de projets"> */
/* <div class="slide" role="group" aria-label="Slide 1 sur 3"> */
5. Éviter overflow: hidden sur le conteneur snap
/* ❌ overflow: hidden désactive le scroll → le snap ne fonctionne plus */
.carousel {
overflow: hidden; /* bloque le scroll ! snap inactif */
scroll-snap-type: x mandatory; /* ignoré car pas de scroll */
}
/* ✅ Pour masquer visuellement le débordement, envelopper dans un parent */
.carousel-wrapper {
overflow: hidden; /* masque le débordement visuellement */
border-radius: 12px; /* s'applique à l'ensemble */
}
.carousel {
overflow-x: scroll; /* scroll activé sur le conteneur */
scroll-snap-type: x mandatory;
display: flex;
}
scroll-snap-typesur le conteneuroverflow: scrollscroll-snap-alignsur les enfants directs du conteneuroverscroll-behavior-x: containsi carousel dans une pageprefers-reduced-motionrespecté- Pas de contenu inaccessible avec
mandatory - Navigation clavier testée (Tab + flèches)
- ARIA
role="region"etaria-labelsur le carousel - Testé sur iOS Safari (comportement inertiel différent)
-webkit-overflow-scrolling: touchajouté pour iOS < 13- Responsive vérifié : mobile, tablette, desktop
Conclusion et ressources
CSS Scroll Snap représente l'une des avancées les plus pratiques du CSS moderne pour les interfaces utilisateurs. En quelques propriétés, il remplace des centaines de lignes de JavaScript fragile et des bibliothèques lourdes, avec de meilleures performances et une accessibilité native.
La clé d'une bonne implémentation tient à trois décisions :
choisir le bon axe (x, y ou both),
la bonne rigueur (mandatory pour les sliders,
proximity pour les listes), et le bon alignement
(start pour les carousels classiques, center pour
l'effet "focus"). Ajoutez overscroll-behavior: contain pour
éviter la propagation du scroll, et scroll-snap-stop: always
quand chaque item doit être vu.
Points clés à retenir
scroll-snap-typesur le conteneur — axe + rigueur (mandatoryouproximity)scroll-snap-alignsur les enfants —start,centerouendscroll-snap-stop: always— empêche de sauter des slidesscroll-padding— décalage pour les headers fixesoverscroll-behavior: contain— évite la propagation du scroll- CSS Snap + JS minimal = meilleure architecture pour les carousels
100svhà la place de100vhpour les sections plein-écran mobiles
Ressources recommandées
- MDN — CSS Scroll Snap — Documentation officielle complète avec tableau de compatibilité et exemples interactifs.
- web.dev — Well-controlled scrolling with CSS Scroll Snap — Guide Google avec cas d'usage avancés et démonstrations.
- MDN — overscroll-behavior — Contrôle avancé du comportement en fin de scroll.
- Can I Use — CSS Scroll Snap — Tableau de compatibilité navigateurs détaillé et mis à jour.