Un peu d'amusement graphique !
Alors… Oui, mais pas n'importe comment.
Cet effet n'est pas anodin pour l'accessibilité, mais aussi pour la performance, notamment pour le Largest Contentful Paint.
La manière la plus simple est d'utiliser une image de fond, un background-attachment fixe, mais ce n'est pas la solution idéale. Il est de loin préférable d'utiliser de vraies images « inline », avec l'avantage supplémentaire de pouvoir gérer les versions responsives.
Cet article propose un script JavaScript efficace, conçu pour implémenter cet effet de parallaxe sur des images, sans dépendance à une librairie externe.
Objectif du Script
Le script a pour objectif d'animer des éléments <img> dans des conteneurs marqués par la classe .parallax en les déplaçant à une vitesse différente du défilement de la page.
Il intègre des fonctionnalités importantes :
- Performance : Utilisation de l'accélération matérielle et optimisation des calculs.
- Accessibilité : Respect de la préférence utilisateur pour la réduction des mouvements.
- Flexibilité : Configuration de la vitesse et de la mise à l'échelle via des attributs de données (
data-*). - Réactivité : Gestion du redimensionnement de la fenêtre.
Fonctionnement et optimisations
Le cœur de la performance et de la fluidité réside dans la gestion des événements et l'application des transformations CSS.
1. Performance et gestion des événements
L'événement de défilement (scroll) est l'un des plus coûteux en ressources. Pour y remédier, le script utilise deux techniques fondamentales :
| Technique | Événement Ciblé | Rôle |
|---|---|---|
requestAnimationFrame |
scroll |
Throttling (limitation) : Assure que les mises à jour CSS sont synchronisées avec le cycle de rendu du navigateur (généralement 60 images par seconde), évitant le déchirement d'image (tearing) et maximisant la fluidité. |
setTimeout |
resize |
Debounce : Retarde l'exécution de la fonction principale jusqu'à ce que l'utilisateur ait fini de redimensionner la fenêtre, prévenant l'exécution inutile de calculs intenses. |
L'écouteur scroll est marqué avec l'option {passive: true}, permettant aux navigateurs de ne pas attendre l'exécution du gestionnaire, améliorant la réactivité du défilement.
2. Accélération Matérielle
Toutes les animations sont appliquées via la propriété CSS transform :
img.style.transform = `translate3d(0, -${offset}px, 0) scale(${scale})`;
L'utilisation de translate3d (même si le z est nul) force le navigateur à déplacer les calculs de transformation vers le GPU (carte graphique), ce qui résulte en une animation beaucoup plus fluide et moins gourmande en ressources CPU.
3. Élagage
Pour éviter de calculer la position d'images invisibles, le script effectue une vérification de la boîte englobante (getBoundingClientRect).
Accessibilité (Accessibility)
Le script met l'accent sur l'accessibilité en respectant la préférence utilisateur pour les animations :
if (
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|| document.documentElement.classList.contains('no-motion')
)
return;
Si l'utilisateur a activé l'option "Réduire les animations" dans les paramètres de son système d'exploitation, le script s'arrête immédiatement. Ceci prévient la cinétose (mal des transports) ou la distraction chez les utilisateurs sensibles aux mouvements rapides du contenu.
Mise en oeuvre HTML
Pour utiliser ce script, vous devez structurer votre HTML comme suit, en utilisant les attributs optionnels data-parallax-speed et data-parallax-scale sur le conteneur (.parallax) :
<!-- minimum classe parallax -->
<div class="parallax-container">
<picture class="parallax">
<img loading="lazy" decoding="async" src="votre-image.jpg" alt="">
</picture>
</div>
<div class="parallax-container">
<picture class="parallax" data-parallax-speed="0.4" data-parallax-scale="0.2">
<img loading="lazy" decoding="async" src="votre-image.jpg" alt="">
</picture>
</div>
<div class="parallax-container">
<picture class="parallax" data-parallax-speed="-0.3">
<img loading="lazy" decoding="async" src="autre-image.jpg" alt="">
</picture>
</div>
<div class="parallax-container">
<picture class="parallax" data-parallax-scale="0.5">
<img loading="lazy" decoding="async" src="autre-image.jpg" alt="">
</picture>
</div>
| Attribut | Valeurs | Effet |
|---|---|---|
data-parallax-speed |
de > 0 à 0.7 |
Classique : l'image remonte plus vite (effet d'accélération). |
de -0.7 à < 0 |
Inversé : l'image remonte plus lentement que le scroll. | |
à 0 |
L'image reste fixed dans son conteneur |
|
data-parallax-scale |
0 à 1 |
Applique un zoom progressif sur l'image durant son déplacement. |
Javascript
Taille brute : 10,73 kilo-octets (~3,19 kilo-octets gzip)
document.addEventListener('DOMContentLoaded', () => {
'use strict';
// -------------------------------------------------------------------------
// 1. Initialisation et Accessibilité
// -------------------------------------------------------------------------
// Vérifie si le système d'exploitation est configuré pour réduire les animations
// Vérifie la présence d'une classe CSS .no-motion (optionnel, voir le menu d'option d'affichage)
window.matchMedia("(prefers-reduced-motion)").addEventListener('change', () => document.location.reload());
if (
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|| document.documentElement.classList.contains('no-motion')
)
return;
// Sélectionne toutes les images destinées à l'effet de parallaxe (contenues dans .parallax)
const parallaxImages = document.querySelectorAll('.parallax img');
if (parallaxImages.length === 0) return;
// -------------------------------------------------------------------------
// 2. Constantes et Variables de Contrôle
// -------------------------------------------------------------------------
const DEFAULT_SPEED = .25; // Vitesse de parallaxe par défaut (0.25 = 25% de la vitesse de défilement)
const MIN_SPEED = -.7; // Vitesse minimale (parallaxe inversé)
const MAX_SPEED = .7; // Vitesse maximale
const DEFAULT_SCALE = 0; // Facteur de mise à l'échelle par défaut
let ticking = false; // Flag pour le requestAnimationFrame (throttle du scroll)
let resizeTimer = null; // Timer pour le debounce du redimensionnement
/**
* Limite une valeur entre une borne minimale et une borne maximale.
* @param {number} value - La valeur à limiter
* @param {number} min - Valeur minimale
* @param {number} max - Valeur maximale
* @returns {number} La valeur limitée entre min et max
*/
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
// -------------------------------------------------------------------------
// 3. Fonction Principale : Application du Parallaxe
// -------------------------------------------------------------------------
/**
* Calcule et applique les transformations (translate et scale) pour l'effet de parallaxe
* à toutes les images visibles. Optimisé pour la performance.
* @returns {void}
*/
const applyParallax = () => {
const scrollY = window.scrollY; // Position actuelle de défilement vertical
const viewportHeight = window.innerHeight; // Hauteur actuelle de la fenêtre d'affichage
parallaxImages.forEach(img => {
const container = img.closest('.parallax');
if (!container) return;
const rect = container.getBoundingClientRect();
// --- Optimisation de la Visibilité ---
// Vérifie si le conteneur est hors de la vue (avec une marge de 100px)
// Si l'élément est invisible, on ignore le calcul (gain de performance).
const isOutsideViewportVertically = rect.bottom < -100 || rect.top > viewportHeight + 100;
if (container.classList.contains('parallax-carousel')) {
if (isOutsideViewportVertically) {
return
}
} else {
// Pour les conteneurs standard, vérifie aussi l'horizontal (juste au cas où)
if (isOutsideViewportVertically || rect.right < -100 || rect.left > window.innerWidth + 100) {
return
}
}
// --- Récupération des Paramètres Personnalisés ---
const customSpeed = parseFloat(container.dataset.parallaxSpeed);
// Assure que la vitesse est valide et contrainte par MIN/MAX_SPEED
const speed = !isNaN(customSpeed) ? clamp(customSpeed, MIN_SPEED, MAX_SPEED) : DEFAULT_SPEED;
const customScale = parseFloat(container.dataset.parallaxScale);
const scaleRatio = !isNaN(customScale) ? Math.max(0, customScale) : DEFAULT_SCALE;
// --- Calculs de Base ---
const containerHeight = container.offsetHeight;
const imgHeight = containerHeight * 1.5; // Hauteur de l'image (pour permettre le décalage)
const maxOffset = imgHeight - containerHeight; // Décalage vertical maximal possible
// Récupération de la position absolue du conteneur dans le document
const containerTop = rect.top + scrollY;
// ... (Le reste des calculs de progression de scroll inutilisés pour speed=0 est laissé pour les autres cas)
// --- Application de la Transformation ---
if (speed === 0) {
// Cas: Vitesse nulle (Effet Fixed)
// L'image doit rester centrée verticalement dans le conteneur tout au long du scroll.
img.style.objectPosition = 'center center';
// Calcul de l'offset basé sur la position du scroll par rapport au conteneur
// L'image doit suivre le conteneur, mais avec un décalage calculé
// pour la centrer par rapport au milieu de l'effet parallaxe (maxOffset/2)
// Calcul de la distance scrollée DANS le container
const scrollDistanceInContainer = scrollY - containerTop;
// Centre l'image dans l'espace maxOffset disponible
// L'image monte (offset négatif) quand on scroll VERS le bas (scrollY augmente)
// On utilise une fraction de la hauteur du conteneur ou un simple centrage
// Calcul simple pour centrer l'image verticalement (similaire à l'ancien script)
const fixedOffset = clamp(scrollY - containerTop - (containerHeight / 2) + (viewportHeight / 2), -maxOffset / 2, maxOffset / 2);
// Alternative plus simple : centrer l'image verticalement dans le viewport quand on passe dessus
// const offset = clamp(scrollY - containerTop, -maxOffset / 2, maxOffset / 2);
// Pour un vrai effet 'fixed' (l'image ne bouge pas du viewport) :
// On veut que l'image compense le défilement de son conteneur.
// Le décalage doit être la distance entre le haut du viewport et le haut du conteneur.
const offset = clamp(scrollY - containerTop, -maxOffset / 2, maxOffset / 2);
img.style.transform = `translate3d(0,${offset}px,0)`;
} else if (speed < 0) {
// Cas: Vitesse négative (Parallaxe inversé)
// ... (Reste du code speed < 0, en utilisant les variables de progression si souhaité)
// NOTE: Si vous voulez conserver la nouvelle logique de progression (scrollProgress) pour speed < 0 et speed > 0,
// assurez-vous qu'elle est correcte et utilisez-la.
// La nouvelle logique de progression :
// Point de déclenchement : quand le bas du container entre dans le viewport
const triggerPoint = containerTop + containerHeight - viewportHeight;
// Distance scrollée depuis le point de déclenchement
const scrolledFromTrigger = Math.max(0, scrollY - triggerPoint);
// Distance totale pendant laquelle l'effet s'applique
const effectRange = containerHeight + viewportHeight;
// Progression de 0 (entrée) à 1 (sortie)
const scrollProgress = Math.min(scrolledFromTrigger / effectRange, 1);
img.style.objectPosition = 'center bottom';
// L'offset diminue au fur et à mesure du scroll
const rawOffset = maxOffset - (scrollProgress * effectRange * Math.abs(speed));
const offset = clamp(rawOffset, 0, maxOffset);
const progress = offset / maxOffset;
const scale = scaleRatio > 0 ? 1 - (progress * scaleRatio) : 1;
img.style.transform = `translate3d(0,-${offset}px,0) scale(${2 - scale})`;
} else {
// Cas: Vitesse positive (Parallaxe classique)
// ... (Reste du code speed > 0, en utilisant les variables de progression)
// Point de déclenchement : quand le bas du container entre dans le viewport
const triggerPoint = containerTop + containerHeight - viewportHeight;
// Distance scrollée depuis le point de déclenchement
const scrolledFromTrigger = Math.max(0, scrollY - triggerPoint);
// Distance totale pendant laquelle l'effet s'applique
const effectRange = containerHeight + viewportHeight;
// Progression de 0 (entrée) à 1 (sortie)
const scrollProgress = Math.min(scrolledFromTrigger / effectRange, 1);
img.style.objectPosition = 'center center';
// L'offset augmente au fur et à mesure du scroll
const rawOffset = scrollProgress * effectRange * speed;
const offset = clamp(rawOffset, 0, maxOffset);
const progress = offset / maxOffset;
const scale = scaleRatio > 0 ? 1 + (progress * scaleRatio) : 1;
img.style.transform = `translate3d(0,-${offset}px,0) scale(${scale})`;
}
});
};
// -------------------------------------------------------------------------
// 4. Gestion des Événements
// -------------------------------------------------------------------------
/**
* Gère l'événement de défilement (scroll) en utilisant requestAnimationFrame.
* C'est une technique de "throttling" pour optimiser les performances.
* @returns {void}
*/
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
applyParallax();
ticking = false
});
ticking = true
}
};
/**
* Gère l'événement de redimensionnement (resize) en utilisant un debounce.
* Cela assure que applyParallax est appelé seulement 100ms après la fin du redimensionnement.
* @returns {void}
*/
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
// Recalcule et applique le parallaxe après le redimensionnement pour mettre à jour les dimensions
applyParallax();
}, 100);
};
// -------------------------------------------------------------------------
// 5. Initialisation et Écouteurs
// -------------------------------------------------------------------------
// Configuration initiale de l'object-position avant le premier défilement
parallaxImages.forEach((img, index) => {
const container = img.closest('.parallax');
if (!container) return;
const speed = parseFloat(container.dataset.parallaxSpeed) || DEFAULT_SPEED;
img.style.objectPosition = speed < 0 ? 'center bottom' : 'center center'
});
// Application initiale pour positionner les images au chargement
window.requestIdleCallback
? requestIdleCallback(applyParallax)
: setTimeout(applyParallax, 200);
// Ajout de l'écouteur de défilement (passive: true pour une meilleure performance du défilement)
window.addEventListener('scroll', handleScroll, {
passive: true
});
// Ajout de l'écouteur de redimensionnement
window.addEventListener('resize', handleResize);
});
Télécharger kortic-parallax.js
Styles
Taille brute : 866,00 octets (~344,00 octets gzip)
.parallax-container {
position: relative;
}
.parallax {
position: absolute;
inset: 0;
overflow: hidden;
}
@media (prefers-reduced-motion: no-preference) {
:root:not(.no-motion) .parallax {
transform: translateZ(0);
backface-visibility: hidden;
}
:root:not(.no-motion) .parallax img {
height: 150%;
/* object-position géré en JS selon la direction */
object-fit: cover;
width: 100%;
/* Optimisation GPU */
will-change: transform;
}
:root.no-motion .parallax img {
height: 100%;
object-fit: cover;
object-position: center center !important;
transform: none !important;
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.parallax img {
height: 100%;
object-fit: cover;
object-position: center center !important;
transform: none !important;
width: 100%;
}
}
Télécharger kortic-parallax.css
On en remet une petite pour voir que ça fonctionne à n'importe quel endroit du viewport…
<div style="height: max(25vw, 25vh);margin-bottom: var(--gutter);margin-top: var(--gutter)" class="parallax-container">
<picture class="parallax border-radius" data-parallax-speed=".1" data-parallax-scale=".2">
<img loading="lazy" decoding="async" class="border-radius" src="/assets/images/full/1500/25-juillet-2025-13h35.jpg.webp" alt="">
</picture>
</div>
That's all, Folks !