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 App
archivo, 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 div
contenedor 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-type
y 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 x
direcció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 div
es 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-type
entra 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-align
propiedad 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 :hover
estado 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 ref
propiedad está activada CarouserContainerInner
ya 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 update
función. Lo pondremos en useEffect
el 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 element
y 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 ( rect
en 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:
- Necesitamos recorrer y filtrar todos los contenedores secundarios. Podemos utilizar la
children
propiedad disponible en cada nodo. Entonces, convirtámoslo en una matriz y filtremos:
const visibleElements = Array.from(element.children).filter((child) = {}
- 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()
- 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 previousElementSibling
y 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 useCallback
gancho para evitar renderizaciones innecesarias.
A continuación, implementaremos la scrollToElement
funció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],)
scroll
en 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 auto
Conserva 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-label
como 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