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