Un carrusel CSS súper flexible, mejorado con navegación JavaScript

No estoy seguro de usted, pero a menudo me pregunto cómo construir un componente de carrusel de tal manera que pueda volcar fácilmente un montón de elementos en el componente y obtener un carrusel que funcione bien, uno que le permita desplazarse suavemente, navegar con la dinámica botones y responde. Si eso es lo que te gustaría construir, ¡síguenos y trabajaremos juntos en ello!

Esto es lo que pretendemos:

De ahora en adelante, trabajaremos con bastante JavaScript, React y la API DOM.

Primero, iniciemos un nuevo proyecto.

Comencemos arrancando una aplicación React simple con componentes con estilo agregados para darle estilo:

npx create-react-app react-easy-carouselcd react-easy-carouselyarn add styled-componentsyarn installyarn start

El estilo no es realmente el quid de lo que estamos haciendo, así que he preparado un montón de componentes predefinidos para que los usemos nada más sacarlos de la caja:

// App.styled.jsimport styled from 'styled-components'export const H1 = styled('h1')`  text-align: center;  margin: 0;  padding-bottom: 10rem;`export const Relative = styled('div')`  position: relative;`export const Flex = styled('div')`  display: flex;`export const HorizontalCenter = styled(Flex)`  justify-content: center;  margin-left: auto;  margin-right: auto;  max-width: 25rem;`export const Container = styled('div')`  height: 100vh;  width: 100%;  background: #ecf0f1;`export const Item = styled('div')`  color: white;  font-size: 2rem;  text-transform: capitalize;  width: ${({size}) = `${size}rem`};  height: ${({size}) = `${size}rem`};  display: flex;  align-items: center;  justify-content: center;`

Ahora vayamos a nuestro Apparchivo, eliminemos todo el código innecesario y construyamos una estructura básica para nuestro carrusel:

// App.jsimport {Carousel} from './Carousel'function App() {  return (    Container      H1Easy Carousel/H1      HorizontalCenter        Carousel        {/* Put your items here */}        /Carousel      /HorizontalCenter    /Container  )}export default App

Creo que esta estructura es bastante sencilla. Es el diseño básico el que centra el carrusel directamente en el medio de la página.

Ahora, hagamos el componente del carrusel.

Hablemos de la estructura de nuestro componente. Necesitaremos el divcontenedor principal que será nuestra base. Dentro de eso, aprovecharemos el desplazamiento nativo y colocaremos otro bloque que sirva como área de desplazamiento.

// Carousel.js CarouserContainer  CarouserContainerInner    {children}  /CarouserContainerInner/CarouserContainer

Puede especificar el ancho y el alto en el contenedor interior, pero evitaría dimensiones estrictas en favor de algún componente de tamaño encima para mantener las cosas flexibles.

Desplazamiento, al estilo CSS

Queremos que el desplazamiento sea suave para que quede claro que hay una transición entre diapositivas, por lo que usaremos el ajuste de desplazamiento CSS, estableceremos el desplazamiento horizontalmente a lo largo del eje x y ocultaremos la barra de desplazamiento real mientras estamos en ello.

export const CarouserContainerInner = styled(Flex)`  overflow-x: scroll;  scroll-snap-type: x mandatory;  -ms-overflow-style: none;  scrollbar-width: none;  ::-webkit-scrollbar {    display: none;  }    * {    scroll-snap-align: center;  }`

¿Se pregunta qué pasa con scroll-snap-typey scroll-snap-align? Se trata de CSS nativo que nos permite controlar el comportamiento de desplazamiento de tal manera que un elemento "encaja" en su lugar durante el desplazamiento. Entonces, en este caso, configuramos el tipo de ajuste en la xdirección horizontal ( ) y le dijimos al navegador que debe detenerse en una posición de ajuste que está en el centro del elemento.

En otras palabras: desplácese a la siguiente diapositiva y asegúrese de que la diapositiva esté centrada en la vista. Analicemos esto un poco para ver cómo encaja en el panorama general.

Nuestro exterior dives un contenedor flexible que coloca a sus niños (el carrusel se desliza) en una fila horizontal. Esos elementos secundarios fácilmente desbordarán el ancho del contenedor, por lo que lo hemos hecho para que podamos desplazarnos horizontalmente dentro del contenedor. Ahí es donde scroll-snap-typeentra en juego. De Andy Adams en CSS-Tricks Almanac:

El ajuste de desplazamiento se refiere a "bloquear" la posición de la ventana gráfica en elementos específicos de la página a medida que se desplaza la ventana (o un contenedor desplazable). Piense en ello como poner un imán encima de un elemento que se adhiere a la parte superior de la ventana gráfica y obliga a la página a dejar de desplazarse allí mismo.

No podría decirlo mejor yo mismo. Pruebe con él en la demostración de Andy en CodePen.

Pero todavía necesitamos otra propiedad CSS establecida en los elementos secundarios del contenedor (nuevamente, las diapositivas del carrusel) que le indique al navegador dónde debe detenerse el desplazamiento. Andy compara esto con un imán, así que coloquemos ese imán directamente en el centro de nuestras diapositivas. De esa manera, el desplazamiento se "bloquea" en el centro de una diapositiva, lo que permite verlo completamente en el contenedor del carrusel.

¿Esa propiedad? scroll-snap-align.

  * {  scroll-snap-align: center;}

Ya podemos probarlo creando una serie aleatoria de elementos:

const colors = [  '#f1c40f',  '#f39c12',  '#e74c3c',  '#16a085',  '#2980b9',  '#8e44ad',  '#2c3e50',  '#95a5a6',]const colorsArray = colors.map((color) = (  Item    size={20}    style={{background: color, borderRadius: '20px', opacity: 0.9}}    key={color}      {color}  /Item))

Y vertiéndolo directamente en nuestro carrusel:

// App.jsContainer  H1Easy Carousel/H1  HorizontalCenter    Carousel{colorsArray}/Carousel  /HorizontalCenter/Container

También agreguemos algo de espacio a nuestros elementos para que no se vean demasiado apretados. También puede notar que tenemos espacios innecesarios a la izquierda del primer elemento. Podemos agregar un margen negativo para compensarlo.

export const CarouserContainerInner = styled(Flex)`  overflow-x: scroll;  scroll-snap-type: x mandatory;  -ms-overflow-style: none;  scrollbar-width: none;  margin-left: -1rem;  ::-webkit-scrollbar {    display: none;  }    * {    scroll-snap-align: center;    margin-left: 1rem;  }`  

Observe más de cerca la posición del cursor mientras se desplaza. Siempre está centrado. ¡Esa es la scroll-snap-alignpropiedad en funcionamiento!

¡Y eso es! Hemos creado un carrusel increíble donde podemos agregar cualquier cantidad de elementos y simplemente funciona. Tenga en cuenta también que hicimos todo esto en CSS simple, incluso si se creó como una aplicación React. Realmente no necesitábamos React o componentes con estilo para que esto funcionara.

Bonificación: navegación

Podríamos terminar el artículo aquí y seguir adelante, pero quiero ir un poco más allá. Lo que me gusta de lo que tenemos hasta ahora es que es flexible y realiza el trabajo básico de desplazarse por un conjunto de elementos.

Pero es posible que hayas notado una mejora clave en la demostración al comienzo de este artículo: botones que navegan por las diapositivas. Ahí es donde dejaremos el CSS y nos pondremos nuestros sombreros de JavaScript para que esto funcione.

Primero, definamos botones a la izquierda y a la derecha del contenedor del carrusel que, al hacer clic, se desplazan a la diapositiva anterior o siguiente, respectivamente. Estoy usando flechas SVG simples como componentes:

// ArrowLeftexport const ArrowLeft = ({size = 30, color = '#000000'}) = (  svg        width={size}    height={size}    viewBox="0 0 24 24"    fill="none"    stroke={color}    strokeWidth="2"    strokeLinecap="round"    strokeLinejoin="round"      path d="M19 12H6M12 5l-7 7 7 7" /  /svg)// ArrowRightexport const ArrowRight = ({size = 30, color = '#000000'}) = (  svg        width={size}    height={size}    viewBox="0 0 24 24"    fill="none"    stroke={color}    strokeWidth="2"    strokeLinecap="round"    strokeLinejoin="round"      path d="M5 12h13M12 5l7 7-7 7" /  /svg)

Ahora posicionémoslos a ambos lados de nuestro carrusel:

// Carousel.jsLeftCarouselButton  ArrowLeft //LeftCarouselButtonRightCarouselButton  ArrowRight //RightCarouselButton

Agregaremos un poco de estilo que agregue un posicionamiento absoluto a las flechas, de modo que la flecha izquierda se asiente en el borde izquierdo del carrusel y la flecha derecha se asiente en el borde derecho. Se agregan algunas otras cosas para diseñar los botones para que parezcan botones. Además, estamos jugando con el :hoverestado del contenedor del carrusel para que los botones solo se muestren cuando el cursor del usuario pasa sobre el contenedor.

// Carousel.styled.js// Position and style the buttonsexport const CarouselButton = styled('button')`  position: absolute;  cursor: pointer;  top: 50%;  z-index: 1;  transition: transform 0.1s ease-in-out;  background: white;  border-radius: 15px;  border: none;  padding: 0.5rem;`// Display buttons on hoverexport const LeftCarouselButton = styled(CarouselButton)`  left: 0;  transform: translate(-100%, -50%);  ${CarouserContainer}:hover  {    transform: translate(0%, -50%);  }`// Position the buttons to their respective sidesexport const RightCarouselButton = styled(CarouselButton)`  right: 0;  transform: translate(100%, -50%);  ${CarouserContainer}:hover  {    transform: translate(0%, -50%);  }`

Esto es genial. Ahora tenemos botones, pero sólo cuando el usuario interactúa con el carrusel.

¿Pero siempre queremos ver ambos botones? Sería genial si ocultáramos la flecha izquierda cuando estemos en la primera diapositiva y ocultáramos la flecha derecha cuando estemos en la última diapositiva. Es como si el usuario pudiera navegar más allá de esas diapositivas, entonces, ¿por qué crear la ilusión de que pueden hacerlo?

Sugiero crear un gancho que sea responsable de todas las funciones de desplazamiento que necesitamos, ya que tendremos muchas. Además, es una buena práctica separar las preocupaciones funcionales de nuestro componente visual.

Primero, necesitamos obtener la referencia de nuestro componente para poder obtener la posición de las diapositivas. Hagamos eso con ref:

// Carousel.jsconst ref = useRef()const position = usePosition(ref)CarouserContainer  CarouserContainerInner ref={ref}    {children}  /CarouserContainerInner  LeftCarouselButton    ArrowLeft /  /LeftCarouselButton  RightCarouselButton    ArrowRight /  /RightCarouselButton/CarouserContainer

La refpropiedad está activada CarouserContainerInnerya que contiene todos nuestros artículos y nos permitirá hacer los cálculos adecuados.

Ahora implementemos el gancho en sí. Tenemos dos botones. Para que funcionen, debemos realizar un seguimiento de los elementos anteriores y siguientes en consecuencia. La mejor forma de hacerlo es tener un estado para cada uno:

// usePosition.jsexport function usePosition(ref) {  const [prevElement, setPrevElement] = useState(null)  const [nextElement, setNextElement] = useState(null)}

El siguiente paso es crear una función que detecte la posición de los elementos y actualice los botones para ocultarlos o mostrarlos según esa posición.

Llamémoslo la updatefunción. Lo pondremos en useEffectel gancho de React porque, inicialmente, queremos ejecutar esta función cuando el DOM se monte por primera vez. Necesitamos acceso a nuestro contenedor desplazable que está disponible para usar en ref.current property. Lo pondremos en una variable separada llamada elementy comenzaremos obteniendo la posición del elemento en el DOM.

Lo usaremos getBoundingClientRect()aquí también. Esta es una función muy útil porque nos proporciona la posición de un elemento en la ventana gráfica (es decir, ventana) y nos permite continuar con nuestros cálculos.

// usePosition.js useEffect(() = {  // Our scrollable container  const element = ref.current  const update = () = {    const rect = element.getBoundingClientRect()}, [ref])

Hemos realizado muchísimo posicionamiento hasta ahora y getBoundingClientRect()podemos ayudarnos a comprender tanto el tamaño del elemento ( recten este caso) como su posición relativa a la ventana gráfica.

El siguiente paso es un poco complicado ya que requiere un poco de matemáticas para calcular qué elementos son visibles dentro del contenedor.

Primero, necesitamos filtrar cada elemento obteniendo su posición en la ventana gráfica y comparándolo con los límites del contenedor. Luego, verificamos si el límite izquierdo del niño es mayor que el límite izquierdo del contenedor, y lo mismo en el lado derecho.

Si se cumple una de estas condiciones significa que nuestro niño es visible dentro del contenedor. Convirtámoslo en código paso a paso:

  1. Necesitamos recorrer y filtrar todos los contenedores secundarios. Podemos utilizar la childrenpropiedad disponible en cada nodo. Entonces, convirtámoslo en una matriz y filtremos:
const visibleElements = Array.from(element.children).filter((child) = {}
  1. Después de eso, necesitamos obtener la posición de cada elemento usando esa útil getBoundingClientRect()función una vez más:
const childRect = child.getBoundingClientRect()
  1. Ahora demos vida a nuestro dibujo:
rect.left = childRect.left  rect.right = childRect.right

Juntando todo esto, este es nuestro guión:

// usePosition.jsconst visibleElements = Array.from(element.children).filter((child) = {  const childRect = child.getBoundingClientRect()  return rect.left = childRect.left  rect.right = childRect.right})

Una vez que hayamos filtrado los elementos, debemos verificar si un elemento es el primero o el último para saber ocultar el botón izquierdo o derecho en consecuencia. Crearemos dos funciones auxiliares que verifiquen esa condición usando previousElementSiblingy nextElementSibling. De esta manera, podemos ver si hay un hermano en la lista y si es una instancia HTML y, si lo es, lo devolveremos.

Para recibir el primer elemento y devolverlo, debemos tomar el primer elemento de nuestra lista de elementos visibles y verificar si contiene el nodo anterior. Haremos lo mismo para el último elemento de la lista; sin embargo, necesitamos obtener el último elemento de la lista y verificar si contiene el siguiente elemento después de él:

// usePosition.jsfunction getPrevElement(list) {  const sibling = list[0].previousElementSibling  if (sibling instanceof HTMLElement) {    return sibling  }  return sibling}function getNextElement(list) {  const sibling = list[list.length - 1].nextElementSibling  if (sibling instanceof HTMLElement) {    return sibling  }  return null}

Una vez que tengamos esas funciones, finalmente podemos verificar si hay elementos visibles en la lista y luego configurar nuestros botones izquierdo y derecho en el estado:

// usePosition.js if (visibleElements.length  0) {  setPrevElement(getPrevElement(visibleElements))  setNextElement(getNextElement(visibleElements))}

Ahora necesitamos llamar a nuestra función. Además, queremos llamar a esta función cada vez que nos desplazamos por la lista; ahí es cuando queremos detectar la posición del elemento.

// usePosition.jsexport function usePosition(ref) {  const [prevElement, setPrevElement] = useState(null)  const [nextElement, setNextElement] = useState(null)  useEffect(() = {    const element = ref.current    const update = () = {      const rect = element.getBoundingClientRect()      const visibleElements = Array.from(element.children).filter((child) = {        const childRect = child.getBoundingClientRect()        return rect.left = childRect.left  rect.right = childRect.right      })      if (visibleElements.length  0) {        setPrevElement(getPrevElement(visibleElements))        setNextElement(getNextElement(visibleElements))      }    }    update()    element.addEventListener('scroll', update, {passive: true})    return () = {      element.removeEventListener('scroll', update, {passive: true})    }  }, [ref])

Aquí hay una explicación de por qué estamos pasando {passive: true}por allí.

Ahora devolvamos esas propiedades del gancho y actualicemos nuestros botones en consecuencia:

// usePosition.jsreturn {  hasItemsOnLeft: prevElement !== null,  hasItemsOnRight: nextElement !== null,}
// Carousel.js LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}  ArrowLeft //LeftCarouselButtonRightCarouselButton hasItemsOnRight={hasItemsOnRight}  ArrowRight //RightCarouselButton
// Carousel.styled.jsexport const LeftCarouselButton = styled(CarouselButton)`  left: 0;  transform: translate(-100%, -50%);  ${CarouserContainer}:hover  {    transform: translate(0%, -50%);  }  visibility: ${({hasItemsOnLeft}) = (hasItemsOnLeft ? `all` : `hidden`)};`export const RightCarouselButton = styled(CarouselButton)`  right: 0;  transform: translate(100%, -50%);  ${CarouserContainer}:hover  {    transform: translate(0%, -50%);  }  visibility: ${({hasItemsOnRight}) = (hasItemsOnRight ? `all` : `hidden`)};`

Hasta ahora, todo bien. Como verá, nuestras flechas aparecen dinámicamente dependiendo de nuestra ubicación de desplazamiento en la lista de elementos.

Sólo nos queda un último paso para que los botones funcionen. Necesitamos crear una función que acepte el elemento anterior o siguiente al que debe desplazarse.

const scrollRight = useCallback(() = scrollToElement(nextElement), [  scrollToElement,  nextElement,])const scrollLeft = useCallback(() = scrollToElement(prevElement), [  scrollToElement,  prevElement,])

No olvide incluir funciones en el useCallbackgancho para evitar renderizaciones innecesarias.

A continuación, implementaremos la scrollToElementfunción. La idea es bastante simple. Necesitamos tomar el límite izquierdo de nuestro elemento anterior o siguiente (dependiendo del botón en el que se hizo clic), resumirlo con el ancho del elemento, dividido por dos (posición central) y compensar este valor por la mitad del ancho del contenedor. . Eso nos dará la distancia desplazable exacta hasta el centro del elemento siguiente/anterior.

Aquí está eso en código:

// usePosition.js  const scrollToElement = useCallback(  (element) = {    const currentNode = ref.current    if (!currentNode || !element) return    let newScrollPosition    newScrollPosition =      element.offsetLeft +      element.getBoundingClientRect().width / 2 -      currentNode.getBoundingClientRect().width / 2    currentNode.scroll({      left: newScrollPosition,      behavior: 'smooth',    })  },  [ref],)

scrollen realidad hace el desplazamiento por nosotros mientras pasa la distancia precisa a la que necesitamos desplazarnos. Ahora adjuntemos esas funciones a nuestros botones.

// Carousel.js  const {  hasItemsOnLeft,  hasItemsOnRight,  scrollRight,  scrollLeft,} = usePosition(ref)LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft} onClick={scrollLeft}  ArrowLeft //LeftCarouselButtonRightCarouselButton hasItemsOnRight={hasItemsOnRight} onClick={scrollRight}  ArrowRight //RightCarouselButton

¡Bastante agradable!

Como buen ciudadano, deberíamos limpiar un poco nuestro código. Por un lado, podemos tener más control de los elementos pasados ​​con un pequeño truco que envía automáticamente los estilos necesarios para cada niño. La API para niños es bastante genial y vale la pena echarle un vistazo.

CarouserContainerInner ref={ref}  {React.Children.map(children, (child, index) = (    CarouselItem key={index}{child}/CarouselItem  ))}/CarouserContainerInner

Ahora solo necesitamos actualizar nuestros componentes con estilo. flex: 0 0 autoConserva los tamaños originales de los contenedores, por lo que es totalmente opcional.

export const CarouselItem = styled('div')`  flex: 0 0 auto;  // Spacing between items  margin-left: 1rem;`
export const CarouserContainerInner = styled(Flex)`  overflow-x: scroll;  scroll-snap-type: x mandatory;  -ms-overflow-style: none;  scrollbar-width: none;  margin-left: -1rem; // Offset for children spacing  ::-webkit-scrollbar {    display: none;  }  ${CarouselItem}  {    scroll-snap-align: center;  }`

Accesibilidad

Nos preocupamos por nuestros usuarios, por lo que debemos hacer que nuestro componente no solo sea funcional, sino también accesible para que la gente se sienta cómoda usándolo. Aquí hay un par de cosas que sugeriría:

  • Sumando role='region'a resaltar la importancia de esta área.
  • Agregando un area-labelcomo identificador.
  • Agregar etiquetas a nuestros botones para que los lectores de pantalla puedan identificarlos fácilmente como "Anterior" y "Siguiente" e informar al usuario en qué dirección va un botón.
// Carousel.jsCarouserContainer role="region" aria-label="Colors carousel"  CarouserContainerInner ref={ref}    {React.Children.map(children, (child, index) = (      CarouselItem key={index}{child}/CarouselItem    ))}  /CarouserContainerInner    LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}    onClick={scrollLeft}    aria-label="Previous slide      ArrowLeft /  /LeftCarouselButton    RightCarouselButton hasItemsOnRight={hasItemsOnRight}    onClick={scrollRight}    aria-label="Next slide"       ArrowRight /  /RightCarouselButton/CarouserContainer   

¿Más de un carrusel? ¡Ningún problema!

Siéntase libre de agregar carruseles adicionales para ver cómo se comporta con los elementos de diferentes tamaños. Por ejemplo, coloquemos un segundo carrusel que sea solo una serie de números.

const numbersArray = Array.from(Array(10).keys()).map((number) = (  Item size={5} style={{color: 'black'}} key={number}    {number}  /Item))function App() {  return (    Container      H1Easy Carousel/H1      HorizontalCenter        Carousel{colorsArray}/Carousel      /HorizontalCenter      HorizontalCenter        Carousel{numbersArray}/Carousel      /HorizontalCenter    /Container  )}        

Y listo, ¡magia! Deshazte de un montón de elementos y tendrás un carrusel totalmente funcional nada más sacarlo de la caja.


Siéntete libre de modificar esto y usarlo en tus proyectos. Espero sinceramente que este sea un buen punto de partida para usarlo tal cual o mejorarlo aún más para un carrusel más complejo. ¿Preguntas? ¿Ideas? ¡Contáctame en Twitter, GitHub o en los comentarios a continuación!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir