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>
);
}