Retour au catalogue

Gallery Before After

Comparateur avant/apres avec slider draggable.

gallerycomplex Both Responsive a11y
boldcorporateuniversalbeautyreal-estatecentered
Theme
"use client";
import React, { useState, useRef, useCallback } from "react";
import { motion } from "framer-motion";

interface ComparisonItem { id: string; label: string; before: string; after: string; }
interface GalleryBeforeAfterProps { badge?: string; title?: string; comparisons: ComparisonItem[]; }

function ComparisonSlider({ item }: { item: ComparisonItem }) {
  const [pos, setPos] = 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 pct = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
    setPos(pct);
  }, []);

  return (
    <div>
      <p className="text-sm font-medium mb-3" style={{ color: "var(--color-foreground)" }}>{item.label}</p>
      <div ref={containerRef} className="relative aspect-video rounded-[var(--radius-lg,1rem)] overflow-hidden cursor-ew-resize select-none" style={{ backgroundColor: "var(--color-background-alt)" }}
        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 (full) */}
        <div className="absolute inset-0" style={{ backgroundImage: `url(${item.after})`, backgroundSize: "cover", backgroundPosition: "center" }} />
        {/* Before (clipped) */}
        <div className="absolute inset-0" style={{ backgroundImage: `url(${item.before})`, backgroundSize: "cover", backgroundPosition: "center", clipPath: `inset(0 ${100 - pos}% 0 0)` }} />
        {/* Divider */}
        <div className="absolute top-0 bottom-0 w-0.5" style={{ left: `${pos}%`, backgroundColor: "var(--color-foreground-on-dark)" }}>
          <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-10 w-10 rounded-full flex items-center justify-center" style={{ backgroundColor: "var(--color-accent)" }}>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-5 w-5" style={{ color: "var(--color-background)" }}><path d="M8 3L4 7l4 4" /><path d="M16 3l4 4-4 4" /></svg>
          </div>
        </div>
        {/* Labels */}
        <span className="absolute top-3 left-3 text-xs font-bold px-2 py-1 rounded" style={{ backgroundColor: "rgba(0,0,0,0.6)", color: "var(--color-foreground-on-dark)" }}>Avant</span>
        <span className="absolute top-3 right-3 text-xs font-bold px-2 py-1 rounded" style={{ backgroundColor: "rgba(0,0,0,0.6)", color: "var(--color-foreground-on-dark)" }}>Apres</span>
      </div>
    </div>
  );
}

export default function GalleryBeforeAfter({ badge, title, comparisons }: GalleryBeforeAfterProps) {
  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
      <div className="mx-auto max-w-4xl px-[var(--container-padding-x,1.5rem)]">
        <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.5 }} className="text-center max-w-2xl mx-auto mb-12">
          {badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}>{badge}</span>}
          {title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground)" }}>{title}</h2>}
        </motion.div>
        <div className="flex flex-col gap-12">
          {comparisons.map((comp) => (
            <motion.div key={comp.id} initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5 }}>
              <ComparisonSlider item={comp} />
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Gallery Before After — React Gallery Section — Incubator