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

Avis

Before After Carousel — React Before-after Section — Incubator