Retour au catalogue

Before/After Slider

Slider interactif avant/apres avec handle draggable.

specialtymedium Both Responsive a11y
minimalbeautyplumbingreal-estateuniversalcentered
Theme
"use client";

import { useState, useRef, useCallback } from "react";
import { motion } from "framer-motion";
import { GripVertical } from "lucide-react";

interface SpecialtyBeforeAfterSliderProps { title?: string; subtitle?: string; beforeLabel?: string; afterLabel?: string; }
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

export default function SpecialtyBeforeAfterSlider({ title = "Avant / Apres", subtitle = "", beforeLabel = "Avant", afterLabel = "Apres" }: SpecialtyBeforeAfterSliderProps) {
  const [position, setPosition] = useState(50);
  const containerRef = useRef<HTMLDivElement>(null);
  const dragging = useRef(false);

  const handleMove = useCallback((clientX: number) => {
    if (!containerRef.current || !dragging.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const x = ((clientX - rect.left) / rect.width) * 100;
    setPosition(Math.max(0, Math.min(100, x)));
  }, []);

  return (
    <section className="py-20 lg:py-28" style={{ background: "var(--color-background)" }}>
      <div className="mx-auto max-w-4xl px-6">
        <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease }} viewport={{ once: true }} className="text-center mb-10">
          <h2 className="text-3xl md:text-4xl font-bold" style={{ color: "var(--color-foreground)" }}>{title}</h2>
          {subtitle && <p className="mt-3 text-base" style={{ color: "var(--color-foreground-muted)" }}>{subtitle}</p>}
        </motion.div>

        <motion.div
          ref={containerRef}
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, ease, delay: 0.1 }}
          viewport={{ once: true }}
          className="relative aspect-[16/9] rounded-xl overflow-hidden cursor-col-resize select-none"
          style={{ border: "1px solid var(--color-border)" }}
          onMouseDown={() => { dragging.current = true; }}
          onMouseUp={() => { dragging.current = false; }}
          onMouseLeave={() => { dragging.current = false; }}
          onMouseMove={(e) => handleMove(e.clientX)}
          onTouchStart={() => { dragging.current = true; }}
          onTouchEnd={() => { dragging.current = false; }}
          onTouchMove={(e) => handleMove(e.touches[0].clientX)}
        >
          {/* After */}
          <div className="absolute inset-0" style={{ background: "var(--color-background-alt)" }}>
            <span className="absolute bottom-4 right-4 text-xs font-medium px-2 py-1 rounded" style={{ background: "var(--color-background)", color: "var(--color-foreground-muted)" }}>{afterLabel}</span>
          </div>

          {/* Before (clipped) */}
          <div className="absolute inset-0" style={{ clipPath: `inset(0 ${100 - position}% 0 0)`, background: "var(--color-foreground)", opacity: 0.15 }}>
            <span className="absolute bottom-4 left-4 text-xs font-medium px-2 py-1 rounded" style={{ background: "var(--color-background)", color: "var(--color-foreground-muted)" }}>{beforeLabel}</span>
          </div>

          {/* Handle */}
          <div className="absolute top-0 bottom-0 w-0.5 flex items-center justify-center" style={{ left: `${position}%`, background: "var(--color-accent)" }}>
            <div className="w-8 h-8 rounded-full flex items-center justify-center" style={{ background: "var(--color-accent)", color: "var(--color-background)" }}>
              <GripVertical size={14} />
            </div>
          </div>
        </motion.div>
      </div>
    </section>
  );
}

Avis

Before/After Slider — React Specialty Section — Incubator