Retour au catalogue

Before After Fade Overlay

Transition en fondu entre avant/apres controlee par un slider vertical. Overlay progressif avec opacite.

before-aftermedium Both Responsive a11y
elegantminimalbeautyreal-estateagencyuniversalcentered
Theme
"use client";

import { useState, useCallback, useRef } from "react";
import { motion } from "framer-motion";
import { GripHorizontal } from "lucide-react";

interface BeforeAfterFadeOverlayProps {
  title?: string;
  description?: string;
  beforeLabel?: string;
  afterLabel?: string;
  beforeImage?: string;
  afterImage?: string;
  beforeColor?: string;
  afterColor?: string;
}

const EASE = [0.16, 1, 0.3, 1] as const;

export default function BeforeAfterFadeOverlay({
  title = "Transition en fondu",
  description = "Glissez verticalement pour reveler le resultat",
  beforeLabel = "Avant",
  afterLabel = "Apres",
  beforeImage,
  afterImage,
  beforeColor = "var(--color-background-alt)",
  afterColor = "var(--color-accent)",
}: BeforeAfterFadeOverlayProps) {
  const [opacity, setOpacity] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);
  const isDragging = useRef(false);

  const handleMove = useCallback((clientY: number) => {
    if (!containerRef.current || !isDragging.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const y = Math.max(0, Math.min(clientY - rect.top, rect.height));
    setOpacity(y / rect.height);
  }, []);

  const onMouseDown = useCallback(() => {
    isDragging.current = true;
    const onMove = (e: MouseEvent) => handleMove(e.clientY);
    const onUp = () => {
      isDragging.current = false;
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  }, [handleMove]);

  const onTouchStart = useCallback(() => {
    isDragging.current = true;
    const onMove = (e: TouchEvent) => handleMove(e.touches[0].clientY);
    const onEnd = () => {
      isDragging.current = false;
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("touchend", onEnd);
    };
    window.addEventListener("touchmove", onMove);
    window.addEventListener("touchend", onEnd);
  }, [handleMove]);

  const sliderPosition = opacity * 100;

  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>

        <motion.div
          ref={containerRef}
          initial={{ opacity: 0, scale: 0.98 }}
          whileInView={{ opacity: 1, scale: 1 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6, ease: EASE }}
          style={{
            position: "relative",
            width: "100%",
            aspectRatio: "16 / 10",
            borderRadius: "var(--radius-xl)",
            overflow: "hidden",
            cursor: "ns-resize",
            border: "1px solid var(--color-border)",
            userSelect: "none",
          }}
        >
          {/* Before layer */}
          <div style={{ position: "absolute", inset: 0, background: beforeImage ? `url(${beforeImage}) center/cover` : beforeColor, display: "flex", alignItems: "center", justifyContent: "center" }}>
            {!beforeImage && <span style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-foreground-muted)", opacity: 0.5 }}>{beforeLabel}</span>}
          </div>

          {/* After layer with fade */}
          <div style={{ position: "absolute", inset: 0, background: afterImage ? `url(${afterImage}) center/cover` : afterColor, opacity, transition: "opacity 0.05s ease", display: "flex", alignItems: "center", justifyContent: "center" }}>
            {!afterImage && <span style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-foreground-muted)", opacity: 0.5 }}>{afterLabel}</span>}
          </div>

          {/* Horizontal divider line */}
          <div style={{ position: "absolute", left: 0, right: 0, top: `${sliderPosition}%`, height: 2, background: "var(--color-foreground)", transform: "translateY(-50%)", pointerEvents: "none" }} />

          {/* Handle */}
          <div
            onMouseDown={onMouseDown}
            onTouchStart={onTouchStart}
            style={{
              position: "absolute",
              left: "50%",
              top: `${sliderPosition}%`,
              transform: "translate(-50%, -50%)",
              width: 40,
              height: 40,
              borderRadius: "var(--radius-full)",
              background: "var(--color-foreground)",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              cursor: "ns-resize",
              zIndex: 2,
              boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
            }}
          >
            <GripHorizontal style={{ width: 18, height: 18, color: "var(--color-background)" }} />
          </div>

          {/* Labels */}
          <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)" }}>
            {beforeLabel}
          </span>
          <span style={{ position: "absolute", bottom: 12, right: 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)" }}>
            {afterLabel}
          </span>
        </motion.div>
      </div>
    </section>
  );
}

Avis

Before After Fade Overlay — React Before-after Section — Incubator