Retour au catalogue
Before After Carousel
Carrousel de paires avant/apres avec transitions synchronisees. Navigation par fleches et indicateurs.
before-aftercomplex Both Responsive a11y
elegantcorporatebeautyreal-estateagencyuniversalcarousel
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface Pair {
id: string;
label: string;
beforeImage?: string;
afterImage?: string;
beforeColor?: string;
afterColor?: string;
}
interface BeforeAfterCarouselProps {
title?: string;
description?: string;
pairs?: Pair[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function BeforeAfterCarousel({
title = "Nos transformations",
description = "Parcourez nos realisations avant/apres",
pairs = [],
}: BeforeAfterCarouselProps) {
const [current, setCurrent] = useState(0);
const [showAfter, setShowAfter] = useState(false);
const pair = pairs[current];
if (!pair) return null;
const prev = () => { setCurrent((c) => (c - 1 + pairs.length) % pairs.length); setShowAfter(false); };
const next = () => { setCurrent((c) => (c + 1) % pairs.length); setShowAfter(false); };
const btnStyle: React.CSSProperties = {
width: 44, height: 44, borderRadius: "var(--radius-full)", border: "1px solid var(--color-border)", background: "var(--color-background-card)", color: "var(--color-foreground)", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer",
};
return (
<section style={{ padding: "var(--section-padding-y) 0", background: "var(--color-background)" }}>
<div style={{ maxWidth: "var(--container-max-width)", margin: "0 auto", padding: "0 var(--container-padding-x)" }}>
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: EASE }}
style={{ textAlign: "center", marginBottom: "2.5rem" }}
>
<h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(1.75rem, 3.5vw, 2.75rem)", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.75rem" }}>
{title}
</h2>
<p style={{ fontSize: "1.0625rem", color: "var(--color-foreground-muted)" }}>{description}</p>
</motion.div>
<div style={{ position: "relative", maxWidth: 800, margin: "0 auto" }}>
{/* Image area */}
<div style={{ position: "relative", aspectRatio: "3 / 2", borderRadius: "var(--radius-xl)", overflow: "hidden", border: "1px solid var(--color-border)" }}>
<AnimatePresence mode="wait">
<motion.div
key={`${pair.id}-${showAfter ? "after" : "before"}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4, ease: EASE }}
style={{
position: "absolute", inset: 0,
background: showAfter
? (pair.afterImage ? `url(${pair.afterImage}) center/cover` : pair.afterColor || "var(--color-accent)")
: (pair.beforeImage ? `url(${pair.beforeImage}) center/cover` : pair.beforeColor || "var(--color-background-alt)"),
display: "flex", alignItems: "center", justifyContent: "center",
}}
>
{!(showAfter ? pair.afterImage : pair.beforeImage) && (
<span style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-foreground-muted)", opacity: 0.4 }}>
{showAfter ? "Apres" : "Avant"}
</span>
)}
</motion.div>
</AnimatePresence>
{/* State badge */}
<span style={{ position: "absolute", top: 12, left: 12, fontSize: "0.75rem", fontWeight: 600, padding: "0.25rem 0.75rem", borderRadius: "var(--radius-full)", background: "var(--color-background)", color: "var(--color-foreground)", border: "1px solid var(--color-border)", zIndex: 2 }}>
{showAfter ? "Apres" : "Avant"}
</span>
</div>
{/* Controls */}
<div className="flex items-center justify-between" style={{ marginTop: "1.25rem" }}>
<div className="flex items-center gap-3">
<button onClick={prev} style={btnStyle} aria-label="Precedent">
<ChevronLeft style={{ width: 18, height: 18 }} />
</button>
<button onClick={next} style={btnStyle} aria-label="Suivant">
<ChevronRight style={{ width: 18, height: 18 }} />
</button>
<span style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)" }}>
{current + 1} / {pairs.length}
</span>
</div>
<div className="flex items-center gap-2">
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground)" }}>{pair.label}</span>
<button
onClick={() => setShowAfter((v) => !v)}
style={{
fontSize: "0.75rem", fontWeight: 600, padding: "0.375rem 1rem", borderRadius: "var(--radius-full)", background: showAfter ? "var(--color-accent)" : "var(--color-background-card)", color: "var(--color-foreground)", border: "1px solid var(--color-border)", cursor: "pointer",
}}
>
{showAfter ? "Voir avant" : "Voir apres"}
</button>
</div>
</div>
{/* Dot indicators */}
<div className="flex justify-center gap-2" style={{ marginTop: "1rem" }}>
{pairs.map((p, i) => (
<button
key={p.id}
onClick={() => { setCurrent(i); setShowAfter(false); }}
style={{
width: i === current ? 24 : 8, height: 8, borderRadius: "var(--radius-full)", border: "none", cursor: "pointer",
background: i === current ? "var(--color-accent)" : "var(--color-border)",
transition: "all 0.3s ease",
}}
aria-label={`Aller a ${p.label}`}
/>
))}
</div>
</div>
</div>
</section>
);
}