Retour au catalogue

Website Redesign

Comparaison avant/apres d'une refonte de site web avec slider interactif draggable.

before-aftercomplex Both Responsive a11y
boldelegantagencyportfoliocentered
Theme

How to build a draggable before/after comparison slider in React

A before/after slider in React places two panels inside the same container: the 'after' panel is clipped with CSS clip-path `inset(0 0 0 X%)` where X is the drag position. Mouse and touch events update that percentage in state, moving both the clip boundary and a visible handle.

  • Stack: React 18 + Framer Motion 11 + Lucide React, ~270 lines, no extra dependencies.
  • Drag mechanism: raw mouse/touch events on a ref'd container, no Framer Motion drag API needed.
  • The 'after' panel uses CSS clip-path inset() to reveal only the right portion; the left panel is always visible.
  • Optional stats grid below the slider, staggered with Framer Motion on viewport entry via useInView.
  • Accessible: content in both panels is readable text, not images, screen readers get both sides.

BeforeAfterWebsiteRedesign is a drag-to-compare section that lets visitors scrub between an 'old' and a 'new' state of a website UI. A centered handle divides the viewport: pull it left to expose more of the redesign, push it right to reveal the original. Below the slider, optional metric cards reinforce the story with hard numbers.

Anatomy

The component has three stacked regions. At the top, a centered header block with a project badge (Layout icon + project name), a headline and a subtitle fades in on scroll via useInView. Below it sits the comparison container: two absolutely positioned panels inside a relative div. The 'before' panel fills the container fully; the 'after' panel overlays it and is clipped from the left with clip-path. A vertical accent bar at the drag position terminates in a circular GripVertical handle. Each panel lists four elements (header, hero, layout, CTA) with Minus or Plus icons to signal regression vs. improvement. The third region is an optional responsive stats grid that stagger-animates in.

How it works

The trick is a single React state value `sliderPosition` (0–100, initially 50). When the user presses on the handle div, a boolean `isDragging` flips to true. On every mouse/touch move on the container, `handleMove` reads the event clientX, subtracts the container's left edge from getBoundingClientRect, divides by container width and multiplies by 100. That percentage is written straight to state. The 'after' panel receives it as `clipPath: inset(0 0 0 ${sliderPosition}%)` via an inline style, no Framer Motion spring needed here, the update is instant and that keeps the drag feeling physical. The vertical handle div gets `left: ${sliderPosition}%` plus `transform: translateX(-50%)` to stay centered on the boundary.

How to build it in React

  1. Set up the container and track drag state

    Create a ref on the comparison container and two state values: a number for the split position and a boolean for dragging. Attach onMouseDown to the handle and onMouseUp/onMouseLeave to the container so the drag stops when the pointer leaves.

    const sliderRef = useRef<HTMLDivElement>(null);
    const [sliderPosition, setSliderPosition] = useState(50);
    const [isDragging, setIsDragging] = useState(false);
  2. Convert pointer coordinates to a percentage

    In the move handler, get the container bounds with getBoundingClientRect, clamp the clientX offset between 0 and the container width, then divide to get a 0–100 value. Write it to state inside a useCallback so the reference stays stable across renders.

    const handleMove = useCallback((clientX: number) => {
      if (!sliderRef.current) return;
      const rect = sliderRef.current.getBoundingClientRect();
      const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
      setSliderPosition((x / rect.width) * 100);
    }, []);
  3. Clip the 'after' panel with clip-path inset

    Both panels sit absolute with inset:0. The 'before' panel needs no clipping. The 'after' panel gets an inline clipPath that cuts everything left of the slider position. No CSS transition, the update is synchronous with the drag so there is no lag.

    <div style={{
      position: "absolute",
      inset: 0,
      clipPath: `inset(0 0 0 ${sliderPosition}%)`,
    }}>
      {/* after content */}
    </div>
  4. Add touch support

    Touch events mirror the mouse events exactly. On the container, add onTouchMove that reads e.touches[0].clientX and passes it to the same handleMove function. Add onTouchEnd that resets isDragging. That covers mobile without any extra library.

    onTouchMove={(e) => handleMove(e.touches[0].clientX)}
    onTouchEnd={() => setIsDragging(false)}

When to use it

Ideal for agency portfolio pages, case studies and product landing pages where you need to prove a transformation, not just claim it. The slider works as a section in the middle of a longer page; it does not replace a hero. Avoid it when the 'before' and 'after' states are too similar visually, the contrast must be obvious to justify the interaction. On very small screens the 400px fixed height can feel cramped; consider reducing it or switching to a simple tabbed toggle below a breakpoint.

Used by

  • Stripe, Uses side-by-side and reveal comparisons in its payment UI case studies to show merchant dashboard improvements.
  • Figma, Before/after reveal patterns appear in Figma's feature announcement pages to contrast old and new editor experiences.
  • Webflow, Comparison sliders feature in Webflow's customer case studies to demonstrate site performance and design uplift.
  • Shopify, Merchant success stories use before/after visuals to show storefront transformations, sometimes with interactive sliders.

FAQ

Why not use Framer Motion's drag API for the handle?

Raw mouse/touch events on the container are simpler here because the drag needs to be constrained to horizontal movement within a specific element. Framer Motion's drag API adds overhead and requires extra constraint configuration; getBoundingClientRect plus direct state updates is five lines and handles edge cases cleanly.

Can I use images instead of text content in each panel?

Yes. Replace the text rows with a single background-image or an img tag in each panel. The clip-path mechanism is identical regardless of panel content. For images, make sure both panels have the same dimensions so the visual boundary aligns perfectly.

How do I prevent the page from scrolling while dragging on mobile?

Call e.preventDefault() inside the onTouchMove handler. You will need to attach the listener as a non-passive event listener via addEventListener in a useEffect, because React's synthetic onTouchMove is passive by default and cannot be prevented.

The slider jumps when I first click, how do I fix that?

The jump happens when the initial click is not on the handle but elsewhere on the container. To fix it, attach the mousedown listener to the entire container and call handleMove immediately on mousedown before setting isDragging. That snaps the handle to the click position first, then follows the drag smoothly.

"use client";

import { useRef, useState, useCallback } from "react";
import { motion, useInView } from "framer-motion";
import { GripVertical, Layout, TrendingUp, Minus, Plus } from "lucide-react";

interface BeforeAfterElements {
  header: string;
  hero: string;
  layout: string;
  cta: string;
}

interface Stat {
  label: string;
  value: string;
}

interface BeforeAfterWebsiteRedesignProps {
  title?: string;
  subtitle?: string;
  projectName?: string;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Before/After Slider, Drag to Compare UI