Seguramente has visto muchos carruseles en aplicaciones actuales. Conocidos por muchos nombres como sliders o rotators, estos elementos web muestran contenido de manera visualmente atractiva, ya sea deslizándolo o rotándolo.

Los carruseles sirven tanto para destacar tu Interfaz de Usuario como para ahorrar espacio y así ofrecer una buena experiencia de usuario.

Los carruseles se han convertido en un elemento fundamental en el diseño UI, frecuentemente usados para mostrar imágenes, testimonios de clientes, entre otros. Son indispensables al momento de crear una interfaz llamativa y dinámica.

En este artículo, nos sumergiremos en el proceso de crear un carrusel de imágenes con React y Framer Motion, guiándote por cada paso para crear un componente visual llamativo e interactivo para tu aplicación.

¿Qué es Framer Motion?

Es una librería de animación de código abierto para aplicaciones React, que se utiliza para crear animaciones responsive y dinámicas para nuestra aplicación web.

Framer motion tiene muchas funcionalidades útiles, como por ejemplo:

  1. Animation: Esto permite hacer transiciones fluidas para tus componentes.
  2. Gestures: La librería ofrece soporte para movimientos táctiles y con el ratón. Esto permite dar cuenta de ciertos eventos.
  3. Variants: Framer motion permite declarar componentes de forma declarativa y así mantener tu código ordenado y reutilizable.

Todas estas funcionalidades son muy útiles y pronto veremos cómo utilizarlas.

Para poder entender mejor cómo utilizar Framer Motion, se puede explorar su documentación y recursos [en inglés] aunque en este artículo nos vamos a concentrar en sus elementos fundamentales. Mientras los guío a través de los conceptos básicos de Framer Motion, mi objetivo principal es crear un carrusel atractivo e interactivo.

Cómo preparar tu entorno de desarrollo

Lo primero que vamos a hacer es preparar nuestro entorno de desarrollo. Esto incluye instalar los paquetes necesarios para poder crear tu aplicación, como Node.js y npm. Si ya los tienes instalados, no es necesario que lo hagas de nuevo.

Crear una aplicación React

A esta altura, voy a asumir que ya tienes instalados Node y npm. Para crear una aplicación React, solo debes ir a tu terminal e ir a la ubicación donde quieras alojar tu aplicación.

Luego, ejecuta este comando:

npx create-react-app react-image-carousel

Puedes elegir cualquier nombre para tu aplicación, pero a los fines de este artículo yo le pondré react-image-carousel.

Una vez que hayas creado exitosamente tu aplicación, abre la carpeta en tu editor de código. Deberías tener algunos archivos y estilos por defecto, además de verse así:

Image

Como no vamos a necesitar los siguientes archivos, puedes borrarlos: app.test.js, y logo.svg, and reportWebVitals.js, setupTest.js. También puedes borrar los estilos que vienen por defecto en App.css.

Image

Ahora que tu aplicación React ya está creada y preparada, el último paso para terminar de preparar tu entorno de desarrollo es instalar Framer Motion.

Para hacer esto, simplemente ve a tu terminal y asegúrate que estés en la carpeta de tu proyecto, y luego ejecuta este comando:

 npm install framer-motion

Esto debería instalar la última versión de Framer Motion. Con esto ya estarás listo. Solo ejecuta el comando npm run start e inicia el entorno de desarrollo en tu navegador.

Para empezar con el diseño, vamos a crear primero el componente Carousel.js. Dentro del componente Carousel, vamos a importar el hook useState de React, y luego las propiedades motion y AnimatePresence desde Framer Motion

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

Luego creamos nuestra función Carousel que lleva la prop images, un arreglo con los enlaces de las imágenes:

const Carousel = ({ images }) => {};

En nuestra función Carousel, vamos a inicializar un variable de estado con useState. Para hacer un seguimiento del índice de imagen actual, usamos setCurrentIndex, que es la función para actualizar el índice.

Luego, creamos tres funciones helper que serán las encargadas de manejar las interacciones de usuario. Estas incluyen:

  • handleNext: Esto actualiza el currentIndex al siguiente índice y así poder cambiar la foto. Cuando llega al fin del arreglo, comienza de nuevo.
  • handlePrevious: Hace lo mismo que handleNext, pero en orden inverso, lo que nos permite volver a una imagen.
  • handleDotClick: Esta función lleva un índice como parámetro y actualiza currentIndex. Con esto podemos movernos entre las distintas imágenes solo haciendo clic en los puntos.
const Carousel = ({ images }) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  const handleNext = () => {
    setCurrentIndex((prevIndex) =>
      prevIndex + 1 === images.length ? 0 : prevIndex + 1
    );
  };
  const handlePrevious = () => {
    setCurrentIndex((prevIndex) =>
      prevIndex - 1 < 0 ? images.length - 1 : prevIndex - 1
    );
  };
  const handleDotClick = (index) => {
    setCurrentIndex(index);
  };

Estas son las funciones helper que vamos a necesitar para nuestro componente.

Cómo crear nuestra plantilla

Nuestra plantilla es bastante simple y consiste de nuestra imagen, el deslizador [slide_directon] y los puntos [indicator]

  return (
    <div className="carousel">
        <img
          key={currentIndex}
          src={images[currentIndex]}
        /><div className="slide_direction">
        <div className="left" onClick={handlePrevious}>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            height="20"
            viewBox="0 96 960 960"
            width="20"
          >
            <path d="M400 976 0 576l400-400 56 57-343 343 343 343-56 57Z" />
          </svg>
        </div>
        <div className="right" onClick={handleNext}>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            height="20"
            viewBox="0 96 960 960"
            width="20"
          >
            <path d="m304 974-56-57 343-343-343-343 56-57 400 400-400 400Z" />
          </svg>
        </div>
      </div>
      <div className="indicator">
        {images.map((_, index) => (
          <div
            key={index}
            className={`dot ${currentIndex === index ? "active" : ""}`}
            onClick={() => handleDotClick(index)}
          ></div>
        ))}
      </div>
    </div>
  );

Como pueden ver en esta plantilla, se muestra la imagen del índice actual. Luego, tenemos un div con la clase slider_direction que contiene otros dos divs con las clases left y right. Ambos serán los botones de navegación del carrusel y usan SVGs para mostrar los iconos de flechas. Sus funciones onClick están parametrizados con las funciones handlePrevious y handleNext, respectivamente.

También tenemos un div indicator que creamos para mostrar una serie de círculos que representan cada imagen del carrusel. La función recorre el arreglo de imágenes y crea un círculo para cada imagen. De esta forma, se parametriza la clase active para cada círculo al que le corresponda currentIndex.

Después agregamos un onClick para cada círculo que está parametrizado para hacer correr handleDotClick con el índice del círculo.

Por ahora, con esto ya armamos nuestra plantilla. Lo único que falta es exportar el componente Carousel, importarlo en el componente App.js y luego agregar algo de CSS. Luego de esto estaremos listos para empezar a animar.

Por eso, vamos a exportar nuestra función Carousel para nuestro componente Carousel.js.

export default Carousel;

Ya hemos creado nuestro componente Carousel, pero para poder usarlo tenemos que importarlo dentro de nuestro componente Apps.js:

import Carousel from "./Carousel";

Después de hacer esto, ya podemos crear nuestro arreglo de imágenes en el que van a estar todos los enlaces a las imágenes:

const images = [
  "https://images.pexels.com/photos/169647/pexels-photo-169647.jpeg?auto=compress&cs=tinysrgb&w=600",
  "https://images.pexels.com/photos/313782/pexels-photo-313782.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
  "https://images.pexels.com/photos/773471/pexels-photo-773471.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
  "https://images.pexels.com/photos/672532/pexels-photo-672532.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
  "https://images.pexels.com/photos/632522/pexels-photo-632522.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
  "https://images.pexels.com/photos/777059/pexels-photo-777059.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
];

Todas estas son imágenes que obtuve de pexels y las que vamos a usar en este  proyecto.

Luego, agregamos nuestra función App que es donde va a estar la plantilla de nuestra aplicación:

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Carrusel de imagenes usando React y Framer Motion</h1>
      </header>
      <main>
        <Carousel images={images} />
      </main>
    </div>
  );
}
export default App;

Como puedes ver, en nuestro encabezado tenemos una descripción de lo que hace nuestra aplicación.

Luego, en la sección main está nuestro componente, Carousel que lleva una prop del arreglo de imágenes. Si recuerdas, esta es la prop que usamos en ese componente para mostrar la imágenes.

Por último, exportamos nuestro componente App así lo podemos usar en nuestro archivo index.js.

Para verla sin estilos, ejecuta el comando npm run start y la aplicación debería verse así:

Image

Horrible ¿verdad? Estoy de acuerdo contigo, pero con algunas pocas líneas de CSS vamos a transformarlo. Así que vamos.

Cómo agregar CSS

Como no quiero crear una hoja de estilo separada para el componente Carousel, voy a hacer todo el CSS en el archivo App.css. No te olvides de luego importar tu hoja de estilo.

import "./App.css"

Este es nuestro CSS:

@import url("https://fonts.googleapis.com/css2?family=Oswald:wght@600&display=swap");
.App-header {
  font-size: 1rem;
  text-align: center;
  font-family: "Oswald", sans-serif;
  padding-bottom: 2rem;
}
.carousel-images {
  position: relative;
  border-radius: 10px;
  height: 400px;
  max-width: 650px;
  margin: auto;
  overflow: hidden;
}
.carousel-images img {
  width: 99%;
  height: 99%;
  border-radius: 8px;
  border: #ff00008e solid 2px;
}
.slide_direction {
  display: flex;
  justify-content: space-between;
}
.left,
.right {
  background-color: #fb666675;
  color: #fff;
  padding: 10px 8px 8px 13px;
  margin: 0 20px;
  border-radius: 50%;
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto 10px;
  height: 25px;
  width: 25px;
}
.left {
  left: 0;
}
.right {
  right: 0;
}
.carousel-indicator {
  margin-top: 20px;
  display: flex;
  justify-content: center;
  gap: 20px;
}
.dot {
  background-color: #333;
  width: 15px;
  height: 15px;
  border-radius: 50%;
}
.active {
  background-color: #fa2020;
}

Y este el resultado con nuestro CSS aplicado:

Image

Seguramente coincidirás conmigo que así luce mucho mejor, además de ya ser completamente funcional.

Pasemos ahora a agregar animación con Framer Motion así le agregamos un lindo efecto de deslizador.

Antes de empezar a animar con Framer Motion, hay algunos conceptos con los que deberás estar familiarizado porque los vamos a usar bastante dentro de esta sección. Estos conceptos incluyen:

  • Variants son un grupo de propiedades cuyo propósito es definir cómo un elemento debería aparecer o animarse. Es posible crear distintos variants que representen diferentes estados visuales o animaciones para un elemento, como por ejemplo open, closed, hover y del estilo
  • Initial es el estado que tu objeto tendrá antes de que la animación empiece.
  • Animate es el estado animado de tu objeto. Es tan simple como eso.

Volviendo al proyecto, añadiremos nuestras animaciones al componente Carousel. Ya hemos importado las dos propiedades que vamos a necesitar – motion y AnimatePresence.

Voy a dividir esta sección en tres partes porque la animación la añadiremos a las tres partes de nuestro código: la imagen, la dirección del deslizador y el círculo que se usa como indicador.

Animación de la imagen

Para animar la entrada y salida de una imagen, necesitamos envolver nuestro elemento img con un componente AnimationPresence. Esto nos permite agregar animación cuando una imagen entra o sale. Luego añadimos un motion. a nuestra etiqueta:

 <AnimatePresence>
  <motion.img key={currentIndex} src={images[currentIndex]} />
</AnimatePresence>;

Luego nos posicionamos fuera de nuestra plantilla y declaramos nuestras variants:

  const slideVariants = {
    hiddenRight: {
      x: "100%",
      opacity: 0,
    },
    hiddenLeft: {
      x: "-100%",
      opacity: 0,
    },
    visible: {
      x: "0",
      opacity: 1,
      transition: {
        duration: 1,
      },
    },
    exit: {
      opacity: 0,
      scale: 0.8,
      transition: {
        duration: 0.5,
      },
    },
  };

Como puedes ver, los sliderVariants tienen cuatro propiedades:

  • hiddenRight: esto parametriza la opacidad de la imagen en 0 y la ubica del lado derecho del contenedor.
  • hiddenLeft: hace lo mismo que hiddenRight pero la ubica del lado izquierdo.
  • visible: esta es la propiedad que dispara la animación desde donde esté la imagen al centro del contenedor.
  • exit: esta propiedad controla la salida de una imagen de la pantalla al mismo tiempo que otra imagen entra.

Ahora que ya declaramos nuestros variants, ¿cómo podemos prever desde dónde la imagen va a entrar? Para esto necesitamos parametrizar un estado de dirección y que ese estado se actualice de acuerdo con cuál de los slide_direction hizo clic el usuario.

const [direction, setDirection] = useState('left');

Entonces, vamos a establecer que la imagen entre desde la izquierda. Es lo lógico dado que la primera imagen que se mostrará es la primera de la lista. Luego, en nuestra función helper, vamos a establecer la dirección de acuerdo con la dirección clikeada:

  const handleNext = () => {
    setDirection("right");
    setCurrentIndex((prevIndex) =>
      prevIndex + 1 === images.length ? 0 : prevIndex + 1
    );
  };

  const handlePrevious = () => {
    setDirection("left");

    setCurrentIndex((prevIndex) =>
      prevIndex - 1 < 0 ? images.length - 1 : prevIndex - 1
    );
  };

  const handleDotClick = (index) => {
    setDirection(index > currentIndex ? "right" : "left");
    setCurrentIndex(index);
  };

Quizás hayas notado que no solo parametrizamos el estado para  handleNext y handlePrevious. También lo hicimos para handleDotClick. Así cuando se clickea un círculo anterior o siguiente, la dirección se actualizará correctamente.

Pero recuerda: la función de direction es la de establecer el estado inicial de la imagen para que el deslizador funcione correctamente.

Ya parametrizado direction, vamos a usar nuestros variants con nuestro elemento img:

<AnimatePresence>
          <motion.img
            key={currentIndex}
            src={images[currentIndex]}
            variants={slideVariants}
            initial={direction === "right" ? "hiddenRight" : "hiddenLeft"}
            animate="visible"
            exit="exit"
          />
        </AnimatePresence>

Agregamos la prop variants y la parametrizamos igual que a los slideVariants que creamos. Luego, agregamos la prop initial, con la misma sintaxis que un operador ternario. Esto establece que el estado inicial de la imagen sea hiddenRight o hiddenLeft cuando el usuario hace clic en el círculo indicador de la imagen o en el slider_direction.

Después de esto, agregamos la propiedad animate que se encarga de animar la imagen desde su posición inicial hasta la posición establecida en la propiedad visible.

Por último, agregamos nuestra propiedad exit y la establecemos en exit. Esto va a animar la imagen para que salga de la nueva pantalla cuando entra una nueva.

Hay muchas props que puede usar con Framer Motion. Puedes leer la documentación [en inglés] para aprender más sobre ellas.

Ya con esto, nuestra carrusel de imágenes debería funcionar correctamente.

Image

Animación del deslizador y de los círculos indicadores de la imagen

Podríamos detenernos aquí, pero quiero agregar un poco de animación a las direcciones del deslizador y a los los círculos indicadores de la imagen.

  const slidersVariants = {
    hover: {
      scale: 1.2,
      backgroundColor: "#ff00008e",
    },
  }; 
const dotsVariants = {
    initial: {
      y: 0,
    },
    animate: {
      y: -10,
      scale: 1.3,
      transition: { type: "spring", stiffness: 1000, damping: "10" },
    },
    hover: {
      scale: 1.1,
      transition: { duration: 0.2 },
    },
  };

Empezamos creando nuestros variants. Para los slidersVariants, una propiedad hover. Para los dotsVariants tenemos tres propiedades: initial, animate y hover.

Así como lo hicimos como en el elemento img, para poder usar Framer Motion vamos a agregar motion. como prefijo al nombre del elemento.

<div className="slide_direction">
  <motion.div
    variants={slidersVariants}
    whileHover="hover"
    className="left"
    onClick={handlePrevious}
  >
    <svg
      xmlns="http://www.w3.org/2000/svg"
      height="20"
      viewBox="0 96 960 960"
      width="20"
    >
      <path d="M400 976 0 576l400-400 56 57-343 343 343 343-56 57Z" />
    </svg>
  </motion.div>
  <motion.div
    variants={slidersVariants}
    whileHover="hover"
    className="right"
    onClick={handleNext}
  >
    <svg
      xmlns="http://www.w3.org/2000/svg"
      height="20"
      viewBox="0 96 960 960"
      width="20"
    >
      <path d="m304 974-56-57 343-343-343-343 56-57 400 400-400 400Z" />
    </svg>
  </motion.div>
</div>;

Como pueden ver, hemos añadido nuestros variants y los hemos parametrizados igual a slidersVariants. Luego, usamos una nueva propiedad whileHover y la parametrizamos igual que a la propiedad over que especificamos en nuestro objeto slidersVariants.

<motion.div
  key={index}
  className={`dot ${currentIndex === index ? "active" : ""}`}
  onClick={() => handleDotClick(index)}
  initial="initial"
  animate={currentIndex === index ? "animate" : ""}
  whileHover="hover"
  variants={dotsVariants}
></motion.div>;

Aquí no solo agregamos una propiedad whileHover. También agregamos la propiedad initial y la propiedad animate que anima el círculo de la imagen seleccionada así se destaca.

En nuestro objeto slidersVariants, hemos agregado una transición a la animación que hace que haya un pequeño salto cuando ocurre la transición.

Con todo esto junto, nos queda un carrusel de imágenes prolijo. Aquí el resultado final:

Image

Para referencia, aquí tienes el código completo del componente carrusel:

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

const Carousel = ({ images }) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [direction, setDirection] = useState(null);

  const slideVariants = {
    hiddenRight: {
      x: "100%",
      opacity: 0,
    },
    hiddenLeft: {
      x: "-100%",
      opacity: 0,
    },
    visible: {
      x: "0",
      opacity: 1,
      transition: {
        duration: 1,
      },
    },
    exit: {
      opacity: 0,
      scale: 0.8,
      transition: {
        duration: 0.5,
      },
    },
  };
  const slidersVariants = {
    hover: {
      scale: 1.2,
      backgroundColor: "#ff00008e",
    },
  };
  const dotsVariants = {
    initial: {
      y: 0,
    },
    animate: {
      y: -10,
      scale: 1.2,
      transition: { type: "spring", stiffness: 1000, damping: "10" },
    },
    hover: {
      scale: 1.1,
      transition: { duration: 0.2 },
    },
  };

  const handleNext = () => {
    setDirection("right");
    setCurrentIndex((prevIndex) =>
      prevIndex + 1 === images.length ? 0 : prevIndex + 1
    );
  };

  const handlePrevious = () => {
    setDirection("left");

    setCurrentIndex((prevIndex) =>
      prevIndex - 1 < 0 ? images.length - 1 : prevIndex - 1
    );
  };

  const handleDotClick = (index) => {
    setDirection(index > currentIndex ? "right" : "left");
    setCurrentIndex(index);
  };

  return (
    <div className="carousel">
        <div className="carousel-images">
        <AnimatePresence>
          <motion.img
            key={currentIndex}
            src={images[currentIndex]}
            initial={direction === "right" ? "hiddenRight" : "hiddenLeft"}
            animate="visible"
            exit="exit"
            variants={slideVariants}
          />
        </AnimatePresence>
        <div className="slide_direction">
          <motion.div
            variants={slidersVariants}
            whileHover="hover"
            className="left"
            onClick={handlePrevious}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              height="20"
              viewBox="0 96 960 960"
              width="20"
            >
              <path d="M400 976 0 576l400-400 56 57-343 343 343 343-56 57Z" />
            </svg>
          </motion.div>
          <motion.div
            variants={slidersVariants}
            whileHover="hover"
            className="right"
            onClick={handleNext}
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              height="20"
              viewBox="0 96 960 960"
              width="20"
            >
              <path d="m304 974-56-57 343-343-343-343 56-57 400 400-400 400Z" />
            </svg>
          </motion.div>
        </div>
      </div>
      <div className="carousel-indicator">
        {images.map((_, index) => (
          <motion.div
            key={index}
            className={`dot ${currentIndex === index ? "active" : ""}`}
            onClick={() => handleDotClick(index)}
            initial="initial"
            animate={currentIndex === index ? "animate" : ""}
            whileHover="hover"
            variants={dotsVariants}
          ></motion.div>
        ))}
      </div>
    </div>
  );
};
export default Carousel;

Dale una mirada al repositorio en GitHub.

Aquí está el sitio en Netlify.

Solo para que estén al tanto, este código tiene algunos problemas de accesibilidad por lo que no debería ser usado en un ambiente de producción.

Recursos

Entiendo que puede haber algunos términos o sintaxis que no estén del todo claro, sobretodo si eres nuevo a React o usando Framer Motion. Por eso, aquí tienes algunos recursos que recomiendo si quieres aprender más:

Conclusión

En este artículo, vimos el proceso de diseñar un carrusel de imágenes responsive e interactivo, combinando React y Framer Motion, una librería de animación.

Al incorporar componentes como motion y AnimationPresence, fuimos paso a paso costruyendo un carrusel visualmente atractivo, que muestra nuestras imagenes y con transiciones impecables y animaciones interesantes para una experiencia de usuario enriquecedora.