Blog / CSS/JS

Slider comparatif Avant/Apres

Apprenez a creer un slider de comparaison interactif permettant aux utilisateurs de reveler deux etats d'une image ou d'un element. Ideal pour les portfolios, retouches photo et demonstrations produit.

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.

💡
Ce que vous allez apprendre

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.

comparison-slider.html
<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: relative pour que les enfants absolus se positionnent correctement
  • Aspect ratio : Definissez une hauteur fixe ou utilisez aspect-ratio pour 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.

APRES
AVANT
Avant Apres
comparison-slider.css
.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 gauche
  • inset(0 0 0 0) : aucun masque, image entierement visible
  • inset(0 100% 0 0) : masque tout depuis la droite, image invisible
Performance

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.css
.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;
}
comparison-slider.js
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.

APRES
AVANT
vertical-slider.css
.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);
}
vertical-slider.js
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.

APRES
AVANT
css-only-slider.html
<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>
css-only-slider.css
.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;
}
👍
Avantage CSS-only

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.

touch-support.js
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%)
⚠️
passive: true pour les evenements touch

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-resize ou col-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)
reduced-motion.css
/* 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
🎨
Allez plus loin

Retrouvez des sliders de comparaison prets a l'emploi dans notre bibliotheque d'effets, avec des variantes animees et des styles personnalisables.