Introduction aux sliders comparatifs
Les sliders de comparaison avant/apres sont des composants UI extremement populaires pour presenter des transformations visuelles. Que ce soit pour montrer une retouche photo, un avant/apres renovation, ou la difference entre deux versions d'un design, ce pattern offre une interaction intuitive et engageante.
Dans ce tutoriel complet, nous allons explorer plusieurs approches pour creer ce type de slider : de la version JavaScript complete avec support tactile, jusqu'a une version 100% CSS utilisant un input range. Chaque methode a ses avantages selon vos besoins.
La technique CSS clip-path pour reveler une image, le drag JavaScript avec gestion des evenements souris et tactiles, et une approche CSS-only elegante avec input[type="range"].
Structure HTML de base
La structure d'un slider comparatif repose sur deux couches superposees : l'image "apres" en arriere-plan et l'image "avant" par-dessus, partiellement visible grace a un masque CSS. Une poignee permet a l'utilisateur de controler la zone de revelation.
<div class="comparison-slider">
<!-- Couche arriere : image APRES -->
<div class="comparison-layer after">
<img src="after.jpg" alt="Apres"/>
</div>
<!-- Couche avant : image AVANT (masquee) -->
<div class="comparison-layer before">
<img src="before.jpg" alt="Avant"/>
</div>
<!-- Poignee draggable -->
<div class="comparison-handle"></div>
<!-- Labels optionnels -->
<span class="comparison-label before">Avant</span>
<span class="comparison-label after">Apres</span>
</div>
Points cles de la structure
- Ordre des couches : L'image "apres" est en premier dans le DOM mais apparait en arriere grace au z-index
- Position relative : Le conteneur parent doit etre en
position: relativepour que les enfants absolus se positionnent correctement - Aspect ratio : Definissez une hauteur fixe ou utilisez
aspect-ratiopour maintenir les proportions - Overflow hidden : Essentiel pour masquer les parties des images qui debordent
CSS : la magie de clip-path
La propriete CSS clip-path est la cle de ce composant. Elle permet de definir une zone visible pour un element, masquant tout ce qui se trouve en dehors. Pour notre slider, nous utilisons inset() qui fonctionne comme un cadre interieur.
.comparison-slider {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 16 / 10;
overflow: hidden;
border-radius: 12px;
cursor: ew-resize;
user-select: none;
}
.comparison-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.comparison-layer img {
width: 100%;
height: 100%;
object-fit: cover;
}
.comparison-layer.after {
z-index: 1;
}
.comparison-layer.before {
z-index: 2;
/* clip-path: inset(top right bottom left) */
/* Montre 50% gauche de l'image AVANT */
clip-path: inset(0 50% 0 0);
}
Comprendre clip-path: inset()
La fonction inset() prend 4 valeurs dans l'ordre : top, right, bottom, left (comme les marges). Ces valeurs definissent la distance depuis chaque bord vers l'interieur :
inset(0 50% 0 0): masque 50% depuis la droite, montrant la moitie gaucheinset(0 0 0 0): aucun masque, image entierement visibleinset(0 100% 0 0): masque tout depuis la droite, image invisible
clip-path est tres performant car il est accelere par le GPU. C'est bien meilleur que de modifier la width d'un element qui declenche un reflow du layout.
Slider horizontal avec poignee draggable
Maintenant, ajoutons l'interactivite JavaScript. La poignee doit suivre le curseur de l'utilisateur et mettre a jour le clip-path en temps reel. Nous devons gerer les evenements mousedown, mousemove et mouseup.
.comparison-handle {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 4px;
background: white;
transform: translateX(-50%);
z-index: 10;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
}
/* Cercle de la poignee */
.comparison-handle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
background: white;
border-radius: 50%;
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
}
/* Icone fleches */
.comparison-handle::after {
content: '↔';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.2rem;
color: #333;
}
function initComparisonSlider(container) {
const before = container.querySelector('.before');
const handle = container.querySelector('.comparison-handle');
let isDragging = false;
// Calculer la position en pourcentage
function getPercent(clientX) {
const rect = container.getBoundingClientRect();
const x = clientX - rect.left;
return Math.max(0, Math.min(100, (x / rect.width) * 100));
}
// Mettre a jour le slider
function updateSlider(percent) {
before.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
handle.style.left = percent + '%';
}
// Evenements souris
container.addEventListener('mousedown', (e) => {
isDragging = true;
updateSlider(getPercent(e.clientX));
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
updateSlider(getPercent(e.clientX));
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
// Initialisation
initComparisonSlider(document.querySelector('.comparison-slider'));
Explication du code JavaScript
- getPercent() : Calcule la position du curseur en pourcentage de la largeur du conteneur
- Math.max/min : Limite la valeur entre 0 et 100 pour eviter les debordements
- isDragging : Flag pour savoir si l'utilisateur est en train de glisser
- Ecouteurs sur document : Les evenements mousemove et mouseup sont sur le document pour continuer le drag meme si le curseur sort du slider
Slider vertical
Une variante interessante est le slider vertical, ou l'utilisateur glisse de haut en bas. Il suffit d'adapter le clip-path et la position de la poignee. Cette version est parfaite pour les comparaisons portrait ou les timelines.
.comparison-slider.vertical {
cursor: ns-resize;
}
.comparison-slider.vertical .before {
/* Masque depuis le bas au lieu de la droite */
clip-path: inset(0 0 50% 0);
}
.comparison-slider.vertical .comparison-handle {
/* Barre horizontale */
top: 50%;
bottom: auto;
left: 0;
right: 0;
width: auto;
height: 4px;
transform: translateY(-50%);
}
.comparison-slider.vertical .comparison-handle::after {
transform: translate(-50%, -50%) rotate(90deg);
}
function initVerticalSlider(container) {
const before = container.querySelector('.before');
const handle = container.querySelector('.comparison-handle');
let isDragging = false;
function getPercent(clientY) {
const rect = container.getBoundingClientRect();
const y = clientY - rect.top;
return Math.max(0, Math.min(100, (y / rect.height) * 100));
}
function updateSlider(percent) {
// inset: top right bottom left
before.style.clipPath = `inset(0 0 ${100 - percent}% 0)`;
handle.style.top = percent + '%';
}
container.addEventListener('mousedown', (e) => {
isDragging = true;
updateSlider(getPercent(e.clientY));
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
updateSlider(getPercent(e.clientY));
});
document.addEventListener('mouseup', () => isDragging = false);
}
Version CSS-only avec input range
Une approche elegante sans JavaScript consiste a utiliser un <input type="range"> invisible superpose au slider. L'utilisateur interagit avec le range natif, et nous utilisons des variables CSS pour mettre a jour l'affichage.
<div class="comparison-css-only" style="--pos: 50">
<div class="layer after"><img src="after.jpg"/></div>
<div class="layer before"><img src="before.jpg"/></div>
<div class="handle"></div>
<!-- Input range invisible -->
<input
type="range"
min="0"
max="100"
value="50"
oninput="this.parentElement.style.setProperty('--pos', this.value)"
/>
</div>
.comparison-css-only {
position: relative;
overflow: hidden;
--pos: 50; /* Variable CSS */
}
.comparison-css-only .before {
clip-path: inset(0 calc((100 - var(--pos)) * 1%) 0 0);
}
.comparison-css-only .handle {
left: calc(var(--pos) * 1%);
}
/* Input range invisible */
.comparison-css-only input[type="range"] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: ew-resize;
z-index: 20;
-webkit-appearance: none;
}
Cette approche fonctionne sans JavaScript et beneficie du support tactile natif des navigateurs. L'input range gere automatiquement le drag sur mobile sans code supplementaire.
Touch support et accessibilite
Pour une experience complete sur mobile, nous devons ajouter les evenements tactiles. De plus, l'accessibilite est importante : les utilisateurs de clavier doivent pouvoir controler le slider.
function initComparisonSliderFull(container) {
const before = container.querySelector('.before');
const handle = container.querySelector('.comparison-handle');
let isDragging = false;
let currentPercent = 50;
function getPercent(clientX) {
const rect = container.getBoundingClientRect();
return Math.max(0, Math.min(100,
((clientX - rect.left) / rect.width) * 100
));
}
function updateSlider(percent) {
currentPercent = percent;
before.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
handle.style.left = percent + '%';
}
// Souris
container.addEventListener('mousedown', e => {
isDragging = true;
updateSlider(getPercent(e.clientX));
});
document.addEventListener('mousemove', e => {
if (isDragging) updateSlider(getPercent(e.clientX));
});
document.addEventListener('mouseup', () => isDragging = false);
// Touch
container.addEventListener('touchstart', e => {
isDragging = true;
updateSlider(getPercent(e.touches[0].clientX));
}, { passive: true });
container.addEventListener('touchmove', e => {
if (isDragging) {
updateSlider(getPercent(e.touches[0].clientX));
}
}, { passive: true });
container.addEventListener('touchend', () => isDragging = false);
// Clavier (accessibilite)
container.setAttribute('tabindex', '0');
container.setAttribute('role', 'slider');
container.setAttribute('aria-valuemin', '0');
container.setAttribute('aria-valuemax', '100');
container.setAttribute('aria-valuenow', '50');
container.addEventListener('keydown', e => {
const step = e.shiftKey ? 10 : 2;
if (e.key === 'ArrowLeft') {
updateSlider(currentPercent - step);
container.setAttribute('aria-valuenow', currentPercent);
} else if (e.key === 'ArrowRight') {
updateSlider(currentPercent + step);
container.setAttribute('aria-valuenow', currentPercent);
}
});
}
Points importants pour l'accessibilite
- tabindex="0" : Permet au slider de recevoir le focus clavier
- role="slider" : Indique aux lecteurs d'ecran le type de composant
- aria-value* : Communique la valeur actuelle et les limites
- Touches fleches : Permettent de deplacer le slider sans souris
- Shift + fleche : Deplacement plus rapide (10% au lieu de 2%)
L'option { passive: true } ameliore les performances sur mobile en indiquant au navigateur que l'evenement ne sera pas annule avec preventDefault(). Cela permet un scrolling plus fluide.
Bonnes pratiques et conseils
Voici quelques recommandations pour creer des sliders de comparaison professionnels et performants.
Performance
- Optimisez vos images : Utilisez des formats modernes (WebP, AVIF) et des tailles appropriees
- Lazy loading : Chargez les images uniquement quand le slider est visible
- Preferez clip-path : Plus performant que modifier width ou left sur les images
- Evitez les box-shadow complexes : Ils peuvent ralentir le rendu pendant le drag
UX Design
- Position initiale : 50% est le standard, mais adaptez selon le contenu
- Poignee visible : Elle doit clairement indiquer qu'on peut interagir
- Labels : Ajoutez "Avant"/"Apres" pour clarifier
- Cursor : Utilisez
ew-resizeoucol-resize
Cas d'usage ideaux
- Retouche photo et filtres
- Renovation et architecture (avant/apres travaux)
- Comparaison de designs et maquettes
- Demonstration de produits (avec/sans fonctionnalite)
- Evolution temporelle (historique vs actuel)
/* Respect des preferences utilisateur */
@media (prefers-reduced-motion: reduce) {
.comparison-slider * {
transition: none !important;
}
}
/* Focus visible pour accessibilite */
.comparison-slider:focus-visible {
outline: 3px solid #6366f1;
outline-offset: 2px;
}
Conclusion
Le slider de comparaison avant/apres est un composant versatile qui enrichit considerablement l'experience utilisateur. Que vous choisissiez l'approche JavaScript complete pour un controle total, ou la version CSS-only pour sa simplicite, vous disposez maintenant de toutes les techniques necessaires.
Les points cles a retenir :
- clip-path: inset() est votre meilleur ami pour ce type d'effet
- Pensez toujours au support tactile et a l'accessibilite clavier
- La version CSS-only avec input range est parfaite pour les cas simples
- Optimisez vos images pour des performances fluides
Retrouvez des sliders de comparaison prets a l'emploi dans notre bibliotheque d'effets, avec des variantes animees et des styles personnalisables.