Como crear un slider de comparacion con arrastre en React
Un slider de comparacion con arrastre en React recorta un panel "antes" usando un clip-path CSS que se actualiza en tiempo real mientras el usuario arrastra un handle. La API drag de Framer Motion gestiona la entrada de puntero y tactil; useMotionValue y useTransform convierten la posicion del handle en una cadena clip-path en vivo sin provocar re-renders.
- Stack: React 18 + Framer Motion 11, sin libreria de iconos, ~360 lineas en total.
- APIs principales: useMotionValue, useTransform, animate, drag + dragConstraints.
- Animacion de presentacion automatica al montar (izquierda 30%, derecha 70%, vuelta al centro) antes de la primera interaccion del usuario.
- ResizeObserver mantiene sincronizado el ancho del contenedor para que el porcentaje del clip-path siga siendo preciso al redimensionar la ventana.
- Funciona en pantallas tactiles gracias a la unificacion de eventos de puntero de Framer Motion; touchAction: none evita conflictos con el scroll.
El Before After Slider es una seccion React con arrastre que permite a los visitantes tirar fisicamente de un handle para revelar una transformacion: un rediseno, un resultado de producto o el resultado de un servicio. El clip-path se anima en vivo sin provocar re-renders de React, por lo que el arrastre se mantiene fluido incluso en dispositivos menos potentes. Una animacion de demostracion se lanza al montar para que los usuarios comprendan la interaccion sin ninguna instruccion.
Anatomía
El componente tiene tres capas visuales apiladas en posicion absoluta dentro de un contenedor 16:9: el panel "despues" en la base (ancho completo, siempre visible), el panel "antes" encima recortado a la izquierda del handle, y un grupo handle arrastrable (linea vertical + boton circular + icono de doble flecha). Dos badges flotantes estan fijados en las esquinas superiores y aparecen en fundido al montar. Un bloque header con titulo y descripcion se situa encima del slider, animado al entrar en el viewport con whileInView.
Cómo funciona
Un unico useMotionValue (x) de Framer Motion rastrea el desplazamiento horizontal del handle respecto al centro del contenedor. El clip-path del panel antes se deriva mediante useTransform: convierte x en un porcentaje y construye una cadena `inset(0 ${100 - pct}% 0 0)` para que el area recortada se reduzca cuando el handle se mueve a la derecha. La posicion del label del handle se calcula del mismo modo con un porcentaje acotado. El prop drag="x" de Framer Motion con dragConstraints={containerRef} confina el movimiento a los limites del contenedor y gestiona la captura de puntero, los eventos tactiles y la cancelacion del impulso (dragMomentum=false) en un solo prop.
Cómo crearlo en React
Configurar el contenedor y medir su ancho
Crea una ref div para el contenedor del slider y usa un ResizeObserver dentro de useEffect para mantener sincronizado un state containerWidth. Este valor es necesario para convertir el desplazamiento en pixeles del drag en un porcentaje 0-100% para el clip-path. Pon touchAction: none en el contenedor para evitar que el navegador intercepte los drags tactiles como scrolls.
const containerRef = useRef<HTMLDivElement>(null); const [containerWidth, setContainerWidth] = useState(0); useEffect(() => { const el = containerRef.current; if (!el) return; const obs = new ResizeObserver(() => setContainerWidth(el.offsetWidth)); obs.observe(el); setContainerWidth(el.offsetWidth); return () => obs.disconnect(); }, []);Controlar el clip-path desde un unico motion value
Declara un useMotionValue para el desplazamiento horizontal (x, partiendo de 0 = centro). Deriva el clip-path del panel antes con useTransform: convierte x en un porcentaje acotado y luego devuelve una cadena inset(). Haz lo mismo para la posicion izquierda del handle. Ambos se actualizan a 60fps sin tocar el state de React.
const x = useMotionValue(0); const clipRight = useTransform(x, (v) => { const pct = Math.max(0, Math.min(100, 50 + (v / containerWidth) * 100)); return `inset(0 ${100 - pct}% 0 0)`; }); const handleLeft = useTransform(x, (v) => { const pct = Math.max(2, Math.min(98, 50 + (v / containerWidth) * 100)); return `${pct}%`; });Conectar el drag al handle y enlazar el clip-path con el panel antes
Renderiza un motion.div handle con drag="x", dragConstraints={containerRef}, dragElastic={0} y dragMomentum={false}. Pasa el motion value x directamente como su prop de style x. Aplica clipPath={clipRight} al panel antes como un style de motion.div. Los dos quedan ahora sincronizados sin ningun gestor de eventos.
<motion.div drag="x" dragConstraints={containerRef} dragElastic={0} dragMomentum={false} style={{ x }}> {/* handle icon */} </motion.div> <motion.div style={{ clipPath: clipRight, position: "absolute", inset: 0 }}> {/* before content */} </motion.div>Anadir la animacion de introduccion automatica
Usa la funcion imperativa animate() de Framer Motion para ejecutar una secuencia (izquierda, derecha, centro) sobre el motion value x tras un breve retardo, una vez que containerWidth este disponible. Condicionala a un state hasInteracted para que la secuencia se detenga en cuanto el usuario agarra el handle.
useEffect(() => { if (!containerWidth || hasInteracted) return; const half = containerWidth / 2; const timer = setTimeout(() => { animate(x, -half * 0.4, { duration: 0.9, ease: [0.4, 0, 0.2, 1], onComplete: () => animate(x, half * 0.4, { duration: 1.0, ease: [0.4, 0, 0.2, 1], onComplete: () => animate(x, 0, { duration: 0.7 }) }) }); }, 600); return () => clearTimeout(timer); }, [containerWidth, hasInteracted, x]);
Cuándo usarlo
Este slider destaca en cualquier sitio donde una transformacion deba sentirse mas que describirse: herramientas de retoque fotografico, escaparates de diseno antes y despues, resultados de cuidado de la piel y belleza, revelaciones de renovaciones inmobiliarias, o cualquier producto SaaS que muestre un flujo antes/despues. Evitalo en paginas donde el contenido de comparacion no sea lo bastante convincente como para justificar el coste de interaccion, y proporciona siempre imagenes reales de antes y despues en lugar de gradientes de marcador de posicion en produccion.
Usado por
- Shopify, Usa comparaciones antes/despues en los casos de exito de comerciantes para mostrar visualmente las transformaciones de las tiendas.
- Canva, Despliega sliders de comparacion de imagenes en las landing pages de funcionalidades para mostrar el eliminador de fondos con IA y los resultados de edicion de fotos.
- Figma, Usa comparaciones de tipo revelacion en los anuncios de versiones para mostrar lado a lado las mejoras de la interfaz.
- Lightroom (Adobe), La conmutacion antes/despues es un pattern de edicion central en sus aplicaciones web y moviles para comparar fotos en bruto y editadas.
FAQ
Por que usar useMotionValue en lugar del state de React para la posicion del drag?
useMotionValue se actualiza fuera del ciclo de renderizado de React, por lo que cada pixel de drag no provoca un re-render. El clip-path y la posicion del handle se actualizan a la frecuencia de fotogramas de la animacion (hasta 120fps) sin el coste de un setState, por eso el slider se mantiene fluido incluso en hardware menos potente.
Como se compara el enfoque clip-path con usar dos div en posicion absoluta con overflow:hidden?
El enfoque clip-path se compone en la GPU y no provoca recalculos de layout, a diferencia de redimensionar el ancho de una div en cada frame. Tambien gestiona de forma mas limpia la linea de separacion y las formas no rectangulares. La contrapartida es que las animaciones clip-path pueden provocar aliasing subpixel en algunas pantallas en el borde.
Funciona el drag en pantallas tactiles?
Si. La API drag de Framer Motion unifica los eventos de raton y puntero, por lo que el drag tactil funciona sin configuracion adicional. El style touchAction: none en el contenedor es obligatorio para evitar que el navegador se apropie del evento tactil para el scroll de la pagina antes de que Framer Motion pueda gestionarlo.
Como puedo pasar imagenes reales en lugar de las maquetas de marcador de posicion?
Pasa las props beforeImage y afterImage como URL absolutas o rutas relativas. Cuando estan definidas, el componente las renderiza como background-image CSS en center/cover, reemplazando por completo las maquetas de marcador de posicion integradas. Ambas props son opcionales, por lo que el componente funciona como demo visual sin configuracion.