Cree un control deslizante de página completa personalizado con CSS y JavaScript
Publicado: 2022-03-11Trabajo mucho con diseños personalizados de pantalla completa, prácticamente a diario. Por lo general, estos diseños implican una cantidad sustancial de interacción y animación. Ya sea que se trate de una línea de tiempo compleja de transiciones activada por el tiempo o de un conjunto de eventos impulsado por el usuario basado en el desplazamiento, en la mayoría de los casos, la interfaz de usuario requiere más que solo usar una solución de complemento lista para usar con algunos ajustes y cambios. . Por otro lado, veo que muchos desarrolladores de JavaScript tienden a buscar su complemento JS favorito para facilitar su trabajo, aunque es posible que la tarea no necesite todas las campanas y silbatos que proporciona un determinado complemento.
Descargo de responsabilidad: el uso de uno de los muchos complementos disponibles tiene sus ventajas, por supuesto. Obtendrá una variedad de opciones que puede usar para ajustar sus necesidades sin tener que codificar demasiado. Además, la mayoría de los autores de complementos optimizan su código, lo hacen compatible con varios navegadores y plataformas, etc. Pero aún así, obtiene una biblioteca de tamaño completo incluida en su proyecto para tal vez solo una o dos cosas diferentes que proporciona. No digo que usar un complemento de terceros de ningún tipo sea naturalmente algo malo, lo hago a diario en mis proyectos, solo que generalmente es una buena idea sopesar los pros y los contras de cada enfoque tal como es. una buena práctica en la codificación. Cuando se trata de hacer lo suyo de esta manera, se requiere un poco más de conocimiento y experiencia en codificación para saber lo que está buscando, pero al final, debe obtener un fragmento de código que hace una cosa y solo una cosa de la manera. quieres que lo haga
Este artículo tiene como objetivo mostrar un enfoque CSS/JS puro en el desarrollo de un diseño de control deslizante activado por desplazamiento a pantalla completa con animación de contenido personalizado. En este enfoque reducido, cubriré la estructura HTML básica que esperaría que se entregue desde un back-end de CMS, técnicas de diseño de CSS modernas (SCSS) y codificación de JavaScript estándar para una interactividad completa. Al ser básico, este concepto puede extenderse fácilmente a un complemento a mayor escala y/o usarse en una variedad de aplicaciones que no tienen dependencias en su núcleo.
El diseño que vamos a crear es un escaparate de cartera de arquitecto minimalista con imágenes destacadas y títulos de cada proyecto. El control deslizante completo con animaciones se verá así:
Puede ver la demostración aquí, y puede acceder a mi repositorio de Github para obtener más detalles.
Descripción general de HTML
Así que aquí está el HTML básico con el que trabajaremos:
<div> <div class="mask"> <!-- Textual logo will go here --> </div> <div> <div class="slides"> <!-- Featured image slides will go here --> </div> <div class="slides mask"> <!-- Slide titles will go here --> </div> </div> <div> <!-- Static info on the right --> </div> <nav> <!-- Current slide indicator --> </nav> </div>
Un div con la identificación de hero-slider
es nuestro titular principal. En el interior, el diseño se divide en secciones:
- Logotipo (una sección estática)
- Presentación de diapositivas en la que trabajaremos principalmente
- Información (una sección estática)
- Navegación deslizante que indicará la diapositiva actualmente activa, así como el número total de diapositivas
Centrémonos en la sección de presentación de diapositivas, ya que ese es nuestro punto de interés en este artículo. Aquí tenemos dos partes: principal y auxiliar . Main es el div que contiene imágenes destacadas, mientras que aux contiene títulos de imágenes. La estructura de cada diapositiva dentro de estos dos soportes es bastante básica. Aquí tenemos una diapositiva de imagen dentro del soporte principal:
<div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>
El atributo de datos de índice es lo que usaremos para realizar un seguimiento de dónde estamos en la presentación de diapositivas. El div abs-mask que usaremos para crear un efecto de transición interesante y el div slide-image contiene la imagen destacada específica. Las imágenes se representan en línea como si vinieran directamente de un CMS y las configura el usuario final.
De manera similar, el título se desliza dentro del soporte auxiliar:
<h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>
Cada título de diapositiva es una etiqueta H2 con el atributo de datos correspondiente y un enlace para poder conducir a la página única de ese proyecto.
El resto de nuestro HTML también es bastante sencillo. Tenemos un logotipo en la parte superior, información estática que le dice al usuario en qué página se encuentra, alguna descripción y un indicador deslizante actual/total.
Descripción general de CSS
El código CSS fuente está escrito en SCSS, un preprocesador de CSS que luego se compila en CSS normal que el navegador puede interpretar. SCSS le brinda la ventaja de usar variables, selección anidada, mixins y otras cosas interesantes, pero debe compilarse en CSS para que el navegador lea el código como debería. Para el propósito de este tutorial, he usado Scout-App para manejar la compilación ya que quería tener las herramientas como mínimo.
Usé flexbox para manejar el diseño básico de lado a lado. La idea es tener la presentación de diapositivas en un lado y la sección de información en el otro.
#hero-slider { position: relative; height: 100vh; display: flex; background: $dark-color; } #slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #info { position: relative; flex: 1 1 $side-width; padding: $offset; background-color: #fff; }
Profundicemos en el posicionamiento y, de nuevo, concentrémonos en la sección de presentación de diapositivas:
#slideshow { position: relative; flex: 1 1 $main-width; display: flex; align-items: flex-end; padding: $offset; } #slides-main { @extend %abs; &:after { content: ''; @extend %abs; background-color: rgba(0, 0, 0, .25); z-index: 100; } .slide-image { @extend %abs; background-position: center; background-size: cover; z-index: -1; } } #slides-aux { position: relative; top: 1.25rem; width: 100%; .slide-title { position: absolute; z-index: 300; font-size: 4vw; font-weight: 700; line-height: 1.3; @include outlined(#fff); } }
Configuré el control deslizante principal para que esté en una posición absoluta y las imágenes de fondo estiren toda el área usando la propiedad background-size: cover
. Para proporcionar más contraste con los títulos de las diapositivas, he configurado un pseudoelemento absoluto que actúa como una superposición. El control deslizante auxiliar que contiene los títulos de las diapositivas se coloca en la parte inferior de la pantalla y encima de las imágenes.
Dado que solo se verá una diapositiva a la vez, configuro cada título para que sea absoluto también, y calculo el tamaño del titular a través de JS para asegurarme de que no haya cortes, pero más sobre eso en una de nuestras próximas secciones. Aquí puede ver el uso de una característica SCSS llamada extensión:
%abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }
Como utilicé mucho el posicionamiento absoluto, puse este CSS en un extensible para tenerlo fácilmente disponible en varios selectores. Además, creé un mixin llamado "delineado" para proporcionar un enfoque SECO al diseñar los títulos y el título del control deslizante principal.
@mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }
En cuanto a la parte estática de este diseño, no tiene nada de complejo, pero aquí puedes ver un método interesante al colocar el texto que debe estar en el eje Y en lugar de su flujo normal:
.slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }
Me gustaría llamar su atención sobre la propiedad transform-origin
ya que la encontré muy poco utilizada para este tipo de diseño. La forma en que se coloca este elemento es que su ancla permanece en la esquina superior izquierda del elemento, estableciendo el punto de rotación y haciendo que el texto fluya continuamente desde ese punto hacia abajo sin problemas cuando se trata de diferentes tamaños de pantalla.
Echemos un vistazo a una parte de CSS más interesante: la animación de carga inicial:
Por lo general, este tipo de comportamiento de animación sincronizada se logra utilizando una biblioteca: GSAP, por ejemplo, es una de las mejores que existen, brinda excelentes capacidades de representación, es fácil de usar y tiene la funcionalidad de línea de tiempo que permite al desarrollador encadenar elementos mediante programación. transiciones entre sí.
Sin embargo, como este es un ejemplo puro de CSS/JS, he decidido ser realmente básico aquí. Por lo tanto, cada elemento se establece en su posición inicial de forma predeterminada, ya sea oculto por transformación u opacidad, y se muestra al cargar el control deslizante que activa nuestro JS. Todas las propiedades de transición se modifican manualmente para garantizar un flujo natural e interesante con cada transición que continúa en otra proporcionando una experiencia visual agradable.
#logo:after { transform: scaleY(0); transform-origin: 50% 0; transition: transform .35s $easing; } .logo-text { display: block; transform: translate3d(120%, 0, 0); opacity: 0; transition: transform .8s .2s, opacity .5s .2s; } .current, .sep:before { opacity: 0; transition: opacity .4s 1.3s; } #info { transform: translate3d(100%, 0, 0); transition: transform 1s $easing .6s; } .line { transform-origin: 0% 0; transform: scaleX(0); transition: transform .7s $easing 1s; } .slider-title { overflow: hidden; >span { display: block; transform: translate3d(0, -100%, 0); transition: transform .5s 1.5s; } }
Si hay algo que me gustaría que viera aquí, es el uso de la propiedad de transform
. Al mover un elemento HTML, ya sea una transición o una animación, se recomienda utilizar la propiedad de transform
. Veo a mucha gente que tiende a utilizar el margen o el relleno o incluso las compensaciones: superior, izquierda, etc., lo que no produce resultados adecuados cuando se trata de renderizar.
Para obtener una comprensión más profunda de cómo usar CSS al agregar un comportamiento interactivo, no podría recomendar lo suficiente el siguiente artículo.
Es de Paul Lewis, un ingeniero de Chrome, y cubre casi todo lo que uno debe saber sobre la representación de píxeles en la web, ya sea CSS o JS.
Descripción general de JavaScript y lógica de control deslizante
El archivo JavaScript se divide en dos funciones distintas.
La función heroSlider
que se encarga de toda la funcionalidad que necesitamos aquí, y la función utils
donde he agregado varias funciones de utilidad reutilizables. He comentado cada una de estas funciones de utilidad para brindar contexto si desea reutilizarlas en su proyecto.
La función principal está codificada de manera que tiene dos ramas: init
y resize
. Estas ramas están disponibles a través del retorno de la función principal y se invocan cuando es necesario. init
es la inicialización de la función principal y se activa en el evento de carga de la ventana. De manera similar, la rama de cambio de tamaño se activa al cambiar el tamaño de la ventana. El único propósito de la función de cambio de tamaño es volver a calcular el tamaño del control deslizante del título en el cambio de tamaño de la ventana, ya que el tamaño de fuente del título puede variar.
En la función heroSlider
, proporcioné un objeto deslizante que contiene todos los datos y selectores que vamos a necesitar:
const slider = { hero: document.querySelector('#hero-slider'), main: document.querySelector('#slides-main'), aux: document.querySelector('#slides-aux'), current: document.querySelector('#slider-nav .current'), handle: null, idle: true, activeIndex: -1, interval: 3500 };
Como nota al margen, este enfoque podría adaptarse fácilmente si, por ejemplo, está usando React, ya que puede almacenar los datos en el estado o usar los ganchos recién agregados. Para mantenernos en el punto, veamos lo que representa cada uno de los pares clave-valor aquí:

- Las primeras cuatro propiedades son una referencia HTML al elemento DOM que manipularemos.
- La propiedad
handle
se usará para iniciar y detener la función de reproducción automática. - La propiedad
idle
es una bandera que evitará que el usuario fuerce el desplazamiento mientras la diapositiva está en transición. -
activeIndex
nos permitirá realizar un seguimiento de la diapositiva actualmente activa -
interval
denota el intervalo de reproducción automática del control deslizante
Tras la inicialización del control deslizante, invocamos dos funciones:
setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();
La función setHeight
llega a una función de utilidad para establecer la altura de nuestro control deslizante auxiliar en función del tamaño máximo del título. De esta manera, nos aseguramos de que se proporcione el tamaño adecuado y que no se corte ningún título de diapositiva, incluso cuando su contenido se reduce a dos líneas.
La función loadingAnimation agrega una clase CSS al elemento que proporciona las transiciones CSS de introducción:
const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }
Como nuestro indicador deslizante es el último elemento en la línea de tiempo de transición de CSS, esperamos a que finalice su transición e invocamos la función de inicio. Al proporcionar un parámetro adicional como objeto, nos aseguramos de que esto se active solo una vez.
Echemos un vistazo a la función de inicio:
const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }
Entonces, cuando el diseño ha terminado, su transición inicial se activa mediante la función loadingAnimation
y la función de inicio se hace cargo. Luego activa la funcionalidad de reproducción automática, habilita el control de la rueda, determina si estamos en un dispositivo táctil o de escritorio, y espera la primera transición de diapositivas de títulos para agregar la clase de CSS adecuada.
Auto-reproducción
Una de las características principales de este diseño es la función de reproducción automática. Repasemos la función correspondiente:
const autoplay = function (initial) { slider.autoplay = true; slider.items = slider.hero.querySelectorAll('[data-index]'); slider.total = slider.items.length / 2; const loop = () => changeSlide('next'); initial && requestAnimationFrame(loop); slider.handle = utils().requestInterval(loop, slider.interval); }
Primero, establecemos el indicador de reproducción automática en verdadero, lo que indica que el control deslizante está en modo de reproducción automática. Este indicador es útil para determinar si se debe volver a activar la reproducción automática después de que el usuario interactúe con el control deslizante. Luego, hacemos referencia a todos los elementos del control deslizante (diapositivas), ya que cambiaremos su clase activa y calcularemos las iteraciones totales que tendrá el control deslizante sumando todos los elementos y dividiéndolos por dos, ya que tenemos dos diseños de control deslizante sincronizados (principal y auxiliar) pero solo un "control deslizante" per se que los cambia a ambos simultáneamente.
La parte más interesante del código aquí es la función de bucle. Invoca slideChange
, proporcionando la dirección de deslizamiento que veremos en un minuto, sin embargo, la función de bucle se llama un par de veces. Veamos por qué.
Si el argumento inicial se evalúa como verdadero, invocaremos la función de bucle como una devolución de llamada de requestAnimationFrame
. Esto solo sucede con la primera carga del control deslizante que activa el cambio de deslizamiento inmediato. Usando requestAnimationFrame
, ejecutamos la devolución de llamada proporcionada justo antes de que se vuelva a pintar el siguiente cuadro.
Sin embargo, como queremos seguir pasando las diapositivas en el modo de reproducción automática, usaremos una llamada repetida de esta misma función. Esto generalmente se logra con setInterval. Pero en este caso, usaremos una de las funciones de utilidad requestInterval
. Si bien setInterval
funcionaría bien, requestInterval
es un concepto avanzado que se basa en requestAnimationFrame
y proporciona un enfoque de mayor rendimiento. Garantiza que la función se vuelva a activar solo si la pestaña del navegador está activa.
Puede encontrar más información sobre este concepto en este increíble artículo sobre trucos de CSS. Tenga en cuenta que asignamos el valor de retorno de esta función a nuestra propiedad slider.handle
. Esta ID única que devuelve la función está disponible para nosotros y la usaremos para cancelar la reproducción automática más adelante usando cancelAnimationFrame
.
Cambio de diapositiva
La función slideChange
es la función principal en todo el concepto. Cambia las diapositivas ya sea por reproducción automática o por activación del usuario. Es consciente de la dirección del control deslizante, proporciona bucles para que cuando llegue a la última diapositiva pueda continuar con la primera diapositiva. Así es como lo he codificado:
const changeSlide = function (direction) { slider.idle = false; slider.hero.classList.remove('prev', 'next'); if (direction == 'next') { slider.activeIndex = (slider.activeIndex + 1) % slider.total; slider.hero.classList.add('next'); } else { slider.activeIndex = (slider.activeIndex - 1 + slider.total) % slider.total; slider.hero.classList.add('prev'); } //reset classes utils().removeClasses(slider.items, ['prev', 'active']); //set prev const prevItems = [...slider.items] .filter(item => { let prevIndex; if (slider.hero.classList.contains('prev')) { prevIndex = slider.activeIndex == slider.total - 1 ? 0 : slider.activeIndex + 1; } else { prevIndex = slider.activeIndex == 0 ? slider.total - 1 : slider.activeIndex - 1; } return item.dataset.index == prevIndex; }); //set active const activeItems = [...slider.items] .filter(item => { return item.dataset.index == slider.activeIndex; }); utils().addClasses(prevItems, ['prev']); utils().addClasses(activeItems, ['active']); setCurrent(); const activeImageItem = slider.main.querySelector('.active'); activeImageItem.addEventListener('transitionend', waitForIdle, { once: true }); }
La idea es determinar la diapositiva activa en función de su índice de datos que obtuvimos de HTML. Abordemos cada paso:
- Establezca el indicador de inactividad del control deslizante en falso. Esto indica que el cambio de diapositiva está en progreso y que los gestos táctiles y de rueda están deshabilitados.
- La clase CSS de dirección del control deslizante anterior se restablece y buscamos la nueva. El parámetro de dirección se proporciona de forma predeterminada como 'siguiente' si venimos de la función de reproducción automática o por una función
wheelControl
por el usuario: control de rueda otouchControl
. - En función de la dirección, calculamos el índice de la diapositiva activa y proporcionamos la clase CSS de la dirección actual al control deslizante. Esta clase de CSS se utiliza para determinar qué efecto de transición se utilizará (por ejemplo, de derecha a izquierda o de izquierda a derecha)
- Las diapositivas restablecen sus clases de CSS de "estado" (anterior, activo) usando otra función de utilidad que elimina las clases de CSS pero se puede invocar en una lista de nodos, en lugar de solo un elemento DOM. Después, solo se les agregan esas clases de CSS a las diapositivas anteriores y actualmente activas. Esto permite que el CSS apunte solo a esas diapositivas y proporcione una transición adecuada.
-
setCurrent
es una devolución de llamada que actualiza el indicador deslizante en función del índice activo. - Finalmente, esperamos a que finalice la transición de la diapositiva de la imagen activa para activar la devolución de llamada
waitForIdle
que reinicia la reproducción automática si el usuario la interrumpió previamente.
Controles de usuario
Según el tamaño de la pantalla, he agregado dos tipos de controles de usuario: rueda y toque. Rueda de control:
const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }
Aquí, escuchamos la rueda incluso y si el control deslizante está actualmente en modo inactivo (actualmente sin animar un cambio de diapositiva), determinamos la dirección de la rueda, invocamos stopAutoplay
para detener la función de reproducción automática si está en progreso y cambiamos la diapositiva según la dirección. La función stopAutoplay
no es más que una función simple que establece nuestro indicador de reproducción automática en el valor falso y cancela nuestro intervalo invocando la función de utilidad cancelRequestInterval
pasándole el identificador apropiado:
const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }
Similar a wheelControl
, tenemos touchControl
que se encarga de los gestos táctiles:
const touchControl = function () { const touchStart = function (e) { slider.ts = parseInt(e.changedTouches[0].clientX); window.scrollTop = 0; } const touchMove = function (e) { slider.tm = parseInt(e.changedTouches[0].clientX); const delta = slider.tm - slider.ts; window.scrollTop = 0; if (slider.idle) { const direction = delta < 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } } slider.hero.addEventListener('touchstart', touchStart); slider.hero.addEventListener('touchmove', touchMove); }
Escuchamos dos eventos: touchstart
y touchmove
. Luego, calculamos la diferencia. Si devuelve un valor negativo, cambiamos a la siguiente diapositiva ya que el usuario ha deslizado de derecha a izquierda. Por otro lado, si el valor es positivo, lo que significa que el usuario ha deslizado de izquierda a derecha, slideChange
con la dirección pasada como "anterior". En ambos casos, la función de reproducción automática se detiene.
Esta es una implementación de gestos de usuario bastante simple. Para aprovechar esto, podríamos agregar botones anterior/siguiente para activar slideChange
al hacer clic o agregar una lista con viñetas para ir directamente a una diapositiva según su índice.
Recapitulación y reflexiones finales sobre CSS
Así que ahí lo tienes, una forma pura de CSS/JS de codificar un diseño de control deslizante no estándar con efectos de transición modernos.
Espero que encuentre útil este enfoque como una forma de pensar y que pueda usar algo similar en sus proyectos front-end al codificar para un proyecto que no fue necesariamente diseñado de manera convencional.
Para aquellos de ustedes interesados en el efecto de transición de imagen, repasaré esto en las próximas líneas.
Si revisamos la estructura HTML de las diapositivas que proporcioné en la sección de introducción, veremos que cada diapositiva de imagen tiene un div
a su alrededor con la clase CSS de abs-mask
. Lo que hace este div
es que oculta una parte de la imagen visible en una cierta cantidad usando overflow:hidden
y desplazándola en una dirección diferente a la de la imagen. Por ejemplo, si observamos la forma en que está codificada la diapositiva anterior:
&.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }
La diapositiva anterior tiene un desplazamiento de -100 % en su eje X, moviéndola hacia la izquierda de la diapositiva actual; sin embargo, el div interno abs-mask
se traslada un 80 % hacia la derecha, proporcionando una ventana de visualización más estrecha. Esto, junto con tener un índice z más grande para la diapositiva activa, da como resultado una especie de efecto de cobertura: la imagen activa cubre la anterior y, al mismo tiempo, extiende su área visible moviendo la máscara que proporciona la vista completa.