Développement Web

Comment créer un effet parallax avec CSS et JavaScript

13 دقائق للقراءة

Prérequis

  • Niveau : bases CSS (position, transform), notions JavaScript (événement scroll).
  • Outils : VS Code + Live Server, navigateur moderne avec DevTools (onglet Performance pour vérifier 60fps).
  • Temps estimé : 1 h.

Pourquoi un effet parallax ?

Le parallax ajoute une profondeur visuelle immédiate à une page web. Bien dosé (1 à 3 sections, jamais sur tout le site), il rend la page mémorable. Mal dosé, il fatigue l’œil et nuit à la performance. À utiliser comme une épice, pas comme plat principal.

L’effet parallax : créer une illusion de profondeur

L’effet parallax donne l’impression que l’arrière-plan se déplace plus lentement que le premier plan lors du défilement, créant une sensation de profondeur 3D. C’est un effet populaire sur les landing pages et les portfolios. Ce tutoriel vous montre 3 approches : CSS pur, JavaScript simple, et une version performante avec transform.

Méthode 1 : CSS pur avec background-attachment

La méthode la plus simple — une seule propriété CSS :

<section class="parallax-section">
  <div class="parallax-content">
    <h1>Bienvenue à Dakar</h1>
    <p>La capitale du digital en Afrique de l'Ouest</p>
  </div>
</section>
<section class="content-section">
  <p>Contenu normal ici...</p>
</section>
.parallax-section {
  height: 100vh;
  background-image: url('dakar-skyline.jpg');
  background-attachment: fixed;   /* L'image ne bouge pas avec le scroll */
  background-position: center;
  background-size: cover;
  display: flex;
  align-items: center;
  justify-content: center;
}

.parallax-content {
  text-align: center;
  color: white;
  text-shadow: 0 2px 8px rgba(0,0,0,0.5);
}

.content-section {
  padding: 60px 20px;
  background: white;
}

Avantage : zéro JavaScript. Limites : ne fonctionne pas sur iOS Safari (Apple désactive background-attachment: fixed sur mobile pour des raisons de performance).

Méthode 2 : JavaScript avec transform (performant)

Cette méthode fonctionne sur tous les appareils et offre un contrôle précis de la vitesse :

<div class="parallax-wrapper">
  <div class="parallax-bg" id="parallaxBg"></div>
  <div class="parallax-foreground">
    <h1>Votre titre ici</h1>
  </div>
</div>
.parallax-wrapper {
  position: relative;
  height: 100vh;
  overflow: hidden;
}

.parallax-bg {
  position: absolute;
  top: -20%;  /* Extra espace pour le mouvement */
  left: 0;
  width: 100%;
  height: 140%;
  background: url('background.jpg') center/cover;
  will-change: transform;
}

.parallax-foreground {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
}
const parallaxBg = document.getElementById('parallaxBg');

window.addEventListener('scroll', () => {
  requestAnimationFrame(() => {
    const scrolled = window.pageYOffset;
    const speed = 0.5; // 0 = fixe, 1 = vitesse normale, 0.5 = moitié
    parallaxBg.style.transform = 'translateY(' + (scrolled * speed) + 'px)';
  });
});

Pourquoi transform et pas top ou background-position ? Les propriétés transform et opacity sont les seules qui n’entraînent pas de recalcul du layout. Elles sont traitées directement par le GPU, ce qui donne des animations fluides à 60fps même sur des appareils modestes.

Méthode 3 : Parallax multi-couches

const layers = document.querySelectorAll('.parallax-layer');

window.addEventListener('scroll', () => {
  requestAnimationFrame(() => {
    const scrolled = window.pageYOffset;
    
    layers.forEach(layer => {
      const speed = parseFloat(layer.dataset.speed);
      layer.style.transform = 'translateY(' + (scrolled * speed) + 'px)';
    });
  });
});
<div class="parallax-layer" data-speed="0.1">Nuages (très lent)</div>
<div class="parallax-layer" data-speed="0.3">Montagnes (lent)</div>
<div class="parallax-layer" data-speed="0.6">Arbres (moyen)</div>
<div class="parallax-layer" data-speed="1">Premier plan (vitesse normale)</div>

Chaque couche a une vitesse différente via data-speed. Les éléments lointains bougent lentement (0.1), les proches rapidement (1). Cela crée une vraie illusion de profondeur.

Performance : les règles à respecter

  • will-change: transform sur les éléments animés — prévient le navigateur de l’animation à venir
  • requestAnimationFrame au lieu d’appeler directement dans le handler de scroll — synchronise avec le rafraîchissement écran
  • Désactiver sur mobile si le contenu est trop lourd — les performances varient beaucoup
  • Tester avec DevTools Performance — visez 60fps, pas de long tasks
// Désactiver le parallax sur mobile
const isMobile = window.matchMedia('(max-width: 768px)').matches;
if (!isMobile) {
  window.addEventListener('scroll', parallaxHandler);
}

Erreurs fréquentes

Parallax saccadé sur mobile

Cause : mobile + scroll lourd + handler non optimisé.
Solution : désactivez le parallax sur mobile (matchMedia('(max-width: 768px)')) ou réduisez la vitesse à 0.2.

background-attachment: fixed qui ne marche pas sur iOS

Cause : Apple désactive cette propriété sur Safari iOS pour des raisons de performance.
Solution : utilisez la méthode JS avec transform: translateY(...), qui fonctionne partout.

Utilisation de window.pageYOffset

Cause : propriété dépréciée au profit de window.scrollY (alias historique).
Solution : en nouveau code, préférez window.scrollY (plus court, identique en comportement).

Ignorer prefers-reduced-motion

Cause : les utilisateurs sensibles au mouvement reçoivent un effet potentiellement nauséeux.
Solution : testez matchMedia('(prefers-reduced-motion: reduce)').matches et désactivez le parallax dans ce cas.

Exercice

  1. Créez une landing page avec 3 sections parallax alternées avec des sections de contenu normal
  2. Donnez une vitesse de parallax différente à chaque section
  3. Ajoutez un texte qui apparaît en fade-in quand il entre dans le viewport
  4. Désactivez l’effet sur mobile et remplacez par une image statique

Pour explorer plus loin

Étape 1 : Comprendre le parallax avant de coder

Sur un site visité depuis un café du Plateau à Dakar comme depuis un bureau d’Abidjan, l’effet parallax donne une impression de profondeur en faisant défiler l’arrière-plan plus lentement que le contenu de premier plan. Avant d’écrire la moindre ligne, comprenez deux approches : la voie pure CSS qui s’appuie sur perspective et transform-style: preserve-3d, et la voie JavaScript qui pilote la position via IntersectionObserver ou requestAnimationFrame.

La voie CSS est la plus performante car déléguée au compositeur du navigateur. La voie JavaScript reste utile quand vous voulez moduler la vitesse en fonction du scroll réel. Dans ce tutoriel, vous allez combiner les deux pour obtenir un résultat fluide à 60 fps.

Étape 2 : Préparer la structure HTML minimale

Créez un fichier index.html vide. Ajoutez un conteneur scrollable qui servira de référentiel pour la perspective, puis trois couches : un fond éloigné, un calque intermédiaire, un calque de premier plan. Cette hiérarchie est indispensable pour que le navigateur calcule correctement la profondeur.

<div class="parallax">
  <section class="layer back">Arrière-plan</section>
  <section class="layer mid">Calque intermédiaire</section>
  <section class="layer front">Premier plan</section>
</div>

Sauvegardez et ouvrez le fichier dans Chrome. À ce stade, les trois sections s’empilent verticalement sans aucun effet : c’est le comportement attendu avant l’application du CSS 3D.

Étape 3 : Activer la perspective CSS

La règle perspective définit la distance entre l’œil de l’utilisateur et le plan de la scène. Plus la valeur est petite, plus l’effet de profondeur est marqué. transform-style: preserve-3d indique au navigateur que les enfants doivent être positionnés dans le même espace 3D.

.parallax {
  perspective: 8px;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
}
.layer {
  position: relative;
  display: grid;
  place-items: center;
  height: 100vh;
  transform-style: preserve-3d;
}

Rechargez la page : le scroll devient natif et chaque section occupe la hauteur de la fenêtre. La perspective est en place mais aucun translate Z n’est encore appliqué, donc visuellement rien ne change. C’est normal.

Étape 4 : Appliquer translateZ pour décaler les couches

Chaque couche reçoit un translateZ négatif pour la repousser en profondeur, puis un scale pour compenser sa réduction apparente. La formule scale = 1 + (-translateZ / perspective) garde la couche à la bonne taille.

.back  { transform: translateZ(-16px) scale(3); background: #0f172a; }
.mid   { transform: translateZ(-8px)  scale(2); background: #1e3a8a; }
.front { transform: translateZ(0);              background: #2563eb; }

Rechargez et faites défiler : la couche back avance trois fois plus lentement que front, créant l’effet parallax sans une ligne de JavaScript. Si le résultat semble trop intense, augmentez perspective à 12px ou 16px.

Étape 5 : Renforcer avec IntersectionObserver pour les animations

Le pur CSS suffit pour le décalage, mais vous voudrez souvent déclencher des animations quand un calque entre dans le viewport. IntersectionObserver est l’API moderne pour cela : elle évite les écouteurs scroll coûteux.

const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) e.target.classList.add('visible');
  });
}, { threshold: 0.25 });

document.querySelectorAll('.layer').forEach(l => io.observe(l));

L’observer ajoute la classe visible dès qu’un quart du calque est à l’écran. Combinez avec une transition CSS sur l’opacité pour une apparition douce. Vérifiez dans l’onglet Performance de DevTools que la frame rate reste à 60 fps.

Étape 6 : Adapter au mobile et respecter prefers-reduced-motion

Sur un Tecno ou un Infinix utilisé à Yopougon, le parallax peut surcharger un GPU modeste. Ajoutez une media query qui désactive l’effet quand l’utilisateur a coché « Réduire les animations » dans les paramètres système. C’est aussi une exigence d’accessibilité WCAG 2.2.

@media (prefers-reduced-motion: reduce) {
  .parallax { perspective: none; }
  .layer    { transform: none !important; }
}

Testez en activant l’option dans Chrome DevTools (onglet Rendering, ligne « Emulate CSS media feature prefers-reduced-motion »). Le parallax doit disparaître instantanément, le contenu reste lisible.

Étape 7 : Tester et déboguer

Ouvrez DevTools, onglet Performance, démarrez l’enregistrement, faites défiler, arrêtez. Cherchez les barres rouges « Long task » : leur absence confirme que le compositeur fait le travail. Si vous voyez des reflows, vérifiez que vous n’animez pas top ou margin (à bannir, animez uniquement transform et opacity).

Sur le même thème sur la performance front-end, consultez notre guide d’optimisation des performances web et notre tutoriel CSS Grid responsive.

Étape 8 : Ajouter une couche d’images optimisées

Les arrière-plans plats sont parfaits pour comprendre le mécanisme, mais une vraie page parallax utilise des images. Sur un site servi depuis Dakar avec une connexion 4G variable, chaque kilo-octet compte. Convertissez vos images en AVIF ou WebP avec squoosh.app avant de les intégrer, et servez-les via <picture> avec un fallback JPEG.

<section class="layer back">
  <picture>
    <source srcset="ciel.avif" type="image/avif">
    <source srcset="ciel.webp" type="image/webp">
    <img src="ciel.jpg" alt="" loading="lazy" decoding="async">
  </picture>
</section>

L’attribut loading="lazy" retarde le chargement jusqu’à ce que l’image approche du viewport, ce qui économise de la data sur mobile. Vérifiez dans l’onglet Network de DevTools que les images au-delà du premier écran ne se téléchargent qu’au scroll.

Étape 9 : Moduler la vitesse avec requestAnimationFrame

Pour des effets plus fins, par exemple un calque qui dépasse les autres ou qui s’incline légèrement, sortez de la pure CSS et calculez la translation manuellement. requestAnimationFrame synchronise vos calculs avec le cycle de rendu du navigateur, contrairement à setInterval qui désynchronise et provoque du jank.

let ticking = false;
function update() {
  const y = window.scrollY;
  document.querySelector('.front').style.transform =
    `translateY(${y * 0.3}px) rotate(${y * 0.02}deg)`;
  ticking = false;
}
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(update);
    ticking = true;
  }
}, { passive: true });

Le drapeau ticking empêche d’empiler plusieurs requestAnimationFrame par frame. L’option passive: true dit au navigateur que vous n’appellerez pas preventDefault, ce qui débloque le scroll natif. Sur DevTools, vérifiez que les frames restent sous 16,7 ms.

Étape 10 : Mesurer le Core Web Vitals après mise en ligne

Une fois le parallax déployé, ouvrez PageSpeed Insights et entrez l’URL. Surveillez trois métriques : LCP (Largest Contentful Paint) qui doit rester sous 2,5 s, CLS (Cumulative Layout Shift) qui doit rester sous 0,1, et INP (Interaction to Next Paint) qui doit rester sous 200 ms. Si le LCP dérape, c’est souvent l’image de fond qui pèse trop : repassez par squoosh et baissez la qualité à 70.

Le CLS peut grimper si vous animez top ou height. Repassez sur l’étape 7 et confirmez que toutes vos animations utilisent uniquement transform et opacity. Pour l’INP, désactivez tout console.log en production : ils sont gratuits en local mais coûteux à grande échelle.

Étape 8 : Ajouter une couche d’images optimisées

Les arrière-plans plats sont parfaits pour comprendre le mécanisme, mais une vraie page parallax utilise des images. Sur un site servi depuis Dakar avec une connexion 4G variable, chaque kilo-octet compte. Convertissez vos images en AVIF ou WebP avec squoosh.app avant de les intégrer, et servez-les via la balise picture avec un fallback JPEG pour les vieux navigateurs encore actifs sur certains terminaux.

<section class="layer back">
  <picture>
    <source srcset="ciel.avif" type="image/avif">
    <source srcset="ciel.webp" type="image/webp">
    <img src="ciel.jpg" alt="" loading="lazy" decoding="async">
  </picture>
</section>

L’attribut loading lazy retarde le chargement jusqu’à ce que l’image approche du viewport, ce qui économise de la data sur mobile. Vérifiez dans l’onglet Network de DevTools que les images au-delà du premier écran ne se téléchargent qu’au scroll. Sur une 4G simulée à 750 kbps, vous devriez gagner deux à trois secondes sur le first paint.

Étape 9 : Moduler la vitesse avec requestAnimationFrame

Pour des effets plus fins, par exemple un calque qui dépasse les autres ou qui s’incline légèrement, sortez de la pure CSS et calculez la translation manuellement. La fonction requestAnimationFrame synchronise vos calculs avec le cycle de rendu du navigateur, contrairement à setInterval qui désynchronise et provoque du jank visible à l’œil nu.

let ticking = false;
function update() {
  const y = window.scrollY;
  document.querySelector('.front').style.transform =
    'translateY(' + (y * 0.3) + 'px) rotate(' + (y * 0.02) + 'deg)';
  ticking = false;
}
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(update);
    ticking = true;
  }
}, { passive: true });

Le drapeau ticking empêche d’empiler plusieurs requestAnimationFrame par frame. L’option passive true dit au navigateur que vous n’appellerez pas preventDefault, ce qui débloque le scroll natif. Sur DevTools, vérifiez que les frames restent sous 16,7 millisecondes pour garantir un rendu à 60 images par seconde.

Étape 10 : Mesurer Core Web Vitals après mise en ligne

Une fois le parallax déployé, ouvrez PageSpeed Insights et entrez l’URL. Surveillez trois métriques : LCP qui doit rester sous 2,5 secondes, CLS qui doit rester sous 0,1, et INP qui doit rester sous 200 millisecondes. Si le LCP dérape, c’est souvent l’image de fond qui pèse trop : repassez par squoosh et baissez la qualité à 70 pour gagner 30 à 40 pourcent du poids sans perte visible.

Le CLS peut grimper si vous animez top ou height. Repassez sur l’étape 7 et confirmez que toutes vos animations utilisent uniquement transform et opacity. Pour l’INP, désactivez tout console.log en production : ces appels sont gratuits en local mais coûteux à grande échelle, surtout sur les Tecno et Infinix utilisés à Bamako ou Sandaga.

مشاركة