Crie um controle deslizante de página inteira personalizado com CSS e JavaScript

Publicados: 2022-03-11

Eu trabalho muito com layouts personalizados de tela cheia, praticamente diariamente. Normalmente, esses layouts implicam uma quantidade substancial de interação e animação. Seja uma linha de tempo complexa de transições acionadas pelo tempo ou um conjunto de eventos orientado pelo usuário baseado em rolagem, na maioria dos casos, a interface do usuário requer mais do que apenas usar uma solução de plug-in pronta para uso com alguns ajustes e alterações . Por outro lado, vejo que muitos desenvolvedores de JavaScript tendem a buscar seu plugin JS favorito para facilitar seu trabalho, mesmo que a tarefa não precise de todos os sinos e assobios que um determinado plugin fornece.

Isenção de responsabilidade: Usar um dos muitos plugins disponíveis tem suas vantagens, é claro. Você terá uma variedade de opções que pode usar para ajustar às suas necessidades sem ter que fazer muita codificação. Além disso, a maioria dos autores de plugins otimiza seu código, torna-o compatível com vários navegadores e plataformas, e assim por diante. Mas ainda assim, você obtém uma biblioteca em tamanho real incluída em seu projeto para talvez apenas uma ou duas coisas diferentes que ela fornece. Não estou dizendo que usar um plugin de terceiros de qualquer tipo é naturalmente uma coisa ruim, eu faço isso diariamente em meus projetos, apenas que geralmente é uma boa ideia pesar os prós e contras de cada abordagem como é uma boa prática de codificação. Quando se trata de fazer suas próprias coisas dessa maneira, é necessário um pouco mais de conhecimento e experiência de codificação para saber o que você está procurando, mas no final, você deve obter um pedaço de código que faça uma coisa e uma coisa apenas da maneira você quer.

Este artigo tem como objetivo mostrar uma abordagem CSS/JS pura no desenvolvimento de um layout de controle deslizante acionado por rolagem em tela cheia com animação de conteúdo personalizada. Nesta abordagem reduzida, abordarei a estrutura HTML básica que você esperaria que fosse entregue a partir de um back-end CMS, técnicas de layout CSS (SCSS) modernas e codificação JavaScript vanilla para total interatividade. Sendo básico, esse conceito pode ser facilmente estendido para um plug-in de maior escala e/ou usado em uma variedade de aplicativos sem dependências em seu núcleo.

O design que vamos criar é uma vitrine minimalista de portfólio de arquitetos com imagens e títulos em destaque de cada projeto. O controle deslizante completo com animações ficará assim:

Controle deslizante de amostra de um portfólio de arquiteto.

Você pode conferir a demonstração aqui e acessar meu repositório do Github para obter mais detalhes.

Visão geral do HTML

Então aqui está o HTML básico com o qual trabalharemos:

 <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>

Um div com o id de hero-slider é nosso principal titular. No interior, o layout é dividido em seções:

  • Logo (uma seção estática)
  • Apresentação de slides na qual trabalharemos principalmente
  • Informações (uma seção estática)
  • Navegador deslizante que indicará o slide ativo no momento, bem como o número total de slides

Vamos nos concentrar na seção de apresentação de slides, pois esse é o nosso ponto de interesse neste artigo. Aqui temos duas partes - main e aux . Main é o div que contém as imagens em destaque, enquanto o aux contém os títulos das imagens. A estrutura de cada slide dentro desses dois suportes é bem básica. Aqui temos um slide de imagem dentro do suporte principal:

 <div class="slide" data-index="0"> <div class="abs-mask"> <div class="slide-image"> </div> </div> </div>

O atributo de dados de índice é o que usaremos para acompanhar onde estamos na apresentação de slides. A div abs-mask que usaremos para criar um efeito de transição interessante e a div slide-image contém a imagem em destaque específica. As imagens são renderizadas em linha como se viessem diretamente de um CMS e são definidas pelo usuário final.

Da mesma forma, o título desliza dentro do suporte auxiliar:

 <h2 class="slide-title slide" data-index="0"><a href="#">#64 Paradigm</a></h2>

Cada título de slide é uma tag H2 com o atributo de dados correspondente e um link para poder levar à página única desse projeto.

O resto do nosso HTML é bastante simples também. Temos um logotipo na parte superior, informações estáticas que informam ao usuário em qual página ele está, alguma descrição e indicador de corrente/total deslizante.

Visão geral do CSS

O código CSS fonte é escrito em SCSS, um pré-processador CSS que é então compilado em CSS regular que o navegador pode interpretar. O SCSS oferece a vantagem de usar variáveis, seleção aninhada, mixins e outras coisas legais, mas precisa ser compilado em CSS para que o navegador leia o código como deveria. Para o propósito deste tutorial, usei o Scout-App para lidar com a compilação, pois queria ter as ferramentas no mínimo.

Eu usei o flexbox para lidar com o layout básico lado a lado. A ideia é ter a apresentação de slides de um lado e a seção de informações do outro.

 #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; }

Vamos mergulhar no posicionamento e, novamente, focar na seção de apresentação de slides:

 #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); } }

Configurei o controle deslizante principal para estar absolutamente posicionado e fiz com que as imagens de fundo esticassem toda a área usando a propriedade background-size: cover . Para fornecer mais contraste com os títulos dos slides, defini um pseudoelemento absoluto que atua como uma sobreposição. O controle deslizante auxiliar contendo os títulos dos slides está posicionado na parte inferior da tela e na parte superior das imagens.

Como apenas um slide estará visível por vez, defino cada título como absoluto também e calculo o tamanho do suporte via JS para garantir que não haja cortes, mas mais sobre isso em uma de nossas próximas seções. Aqui você pode ver o uso de um recurso SCSS chamado estender:

 %abs { position: absolute; top: 0; left: 0; height: 100%; width: 100%; }

Como usei muito o posicionamento absoluto, puxei esse CSS para um extensível para tê-lo facilmente disponível em vários seletores. Além disso, criei um mixin chamado “outlined” para fornecer uma abordagem DRY ao estilizar os títulos e o título do slider principal.

 @mixin outlined($color: $dark-color, $size: 1px) { color: transparent; -webkit-text-stroke: $size $color; }

Quanto à parte estática deste layout, não tem nada de complexo, mas aqui você pode ver um método interessante ao posicionar o texto que deve estar no eixo Y em vez de seu fluxo normal:

 .slider-title-wrapper { position: absolute; top: $offset; left: calc(100% - #{$offset}); transform-origin: 0% 0%; transform: rotate(90deg); @include outlined; }

Gostaria de chamar sua atenção para a propriedade transform-origin , pois achei muito pouco utilizada para esse tipo de layout. A forma como esse elemento é posicionado é que sua âncora fica no canto superior esquerdo do elemento, definindo o ponto de rotação e fazendo com que o texto flua continuamente desse ponto para baixo sem problemas quando se trata de diferentes tamanhos de tela.

Vamos dar uma olhada em uma parte CSS mais interessante - a animação de carregamento inicial:

Carregar animação para controle deslizante.

Normalmente, esse tipo de comportamento de animação sincronizado é obtido usando uma biblioteca - o GSAP, por exemplo, é um dos melhores, fornecendo excelentes recursos de renderização, é fácil de usar e possui a funcionalidade de linha do tempo que permite ao desenvolvedor encadear programaticamente o elemento transições entre si.

No entanto, como este é um exemplo puro de CSS/JS, decidi ir bem básico aqui. Portanto, cada elemento é definido para sua posição inicial por padrão - oculto por transformação ou opacidade e mostrado na carga do slider que é acionada por nosso JS. Todas as propriedades de transição são ajustadas manualmente para garantir um fluxo natural e interessante, com cada transição continuando em outra, proporcionando uma experiência visual agradável.

 #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; } }

Se tem uma coisa que eu gostaria que você visse aqui, é o uso da propriedade transform . Ao mover um elemento HTML, seja uma transição ou animação, é aconselhável usar a propriedade transform . Eu vejo muitas pessoas que tendem a usar margin ou padding ou mesmo os offsets – top, left, etc. que não produzem resultados adequados quando se trata de renderização.

Para obter uma compreensão mais profunda de como usar CSS ao adicionar comportamento interativo, não poderia recomendar o artigo a seguir o suficiente.

É de Paul Lewis, um engenheiro do Chrome, e abrange praticamente tudo o que se deve saber sobre renderização de pixels na Web, seja CSS ou JS.

Visão geral do JavaScript e lógica do controle deslizante

O arquivo JavaScript é dividido em duas funções distintas.

A função heroSlider que cuida de todas as funcionalidades que precisamos aqui, e a função utils onde adicionei várias funções utilitárias reutilizáveis. Eu comentei cada uma dessas funções utilitárias para fornecer contexto se você estiver procurando reutilizá-las em seu projeto.

A função main é codificada de forma que tenha duas ramificações: init e resize . Essas ramificações estão disponíveis via retorno da função principal e são invocadas quando necessário. init é a inicialização da função principal e é acionado no evento de carregamento da janela. Da mesma forma, a ramificação de redimensionamento é acionada no redimensionamento da janela. O único propósito da função de redimensionamento é recalcular o tamanho do controle deslizante do título no redimensionamento da janela, pois o tamanho da fonte do título pode variar.

Na função heroSlider , forneci um objeto slider que contém todos os dados e seletores que vamos precisar:

 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 uma nota lateral, essa abordagem pode ser facilmente adaptada se você estiver, por exemplo, usando o React, pois você pode armazenar os dados no estado ou usar os ganchos recém-adicionados. Para ficar no ponto, vamos apenas ver o que cada um dos pares de valores-chave aqui representa:

  • As primeiras quatro propriedades são uma referência HTML ao elemento DOM que iremos manipular.
  • A propriedade handle será usada para iniciar e parar a funcionalidade de reprodução automática.
  • A propriedade idle é um sinalizador que impedirá o usuário de forçar a rolagem enquanto o slide estiver em transição.
  • activeIndex nos permitirá acompanhar o slide ativo no momento
  • interval denota o intervalo de reprodução automática do controle deslizante

Após a inicialização do slider, invocamos duas funções:

 setHeight(slider.aux, slider.aux.querySelectorAll('.slide-title')); loadingAnimation();

A função setHeight alcança uma função utilitária para definir a altura do nosso controle deslizante auxiliar com base no tamanho máximo do título. Dessa forma, garantimos que o tamanho adequado seja fornecido e nenhum título do slide será cortado, mesmo quando seu conteúdo cair em duas linhas.

A função loadingAnimation adiciona uma classe CSS ao elemento fornecendo as transições CSS de introdução:

 const loadingAnimation = function () { slider.hero.classList.add('ready'); slider.current.addEventListener('transitionend', start, { once: true }); }

Como nosso indicador deslizante é o último elemento na linha de tempo de transição CSS, esperamos que sua transição termine e invocamos a função start. Ao fornecer um parâmetro adicional como um objeto, garantimos que ele seja acionado apenas uma vez.

Vamos dar uma olhada na função start:

 const start = function () { autoplay(true); wheelControl(); window.innerWidth <= 1024 && touchControl(); slider.aux.addEventListener('transitionend', loaded, { once: true }); }

Assim, quando o layout termina, sua transição inicial é acionada pela função loadingAnimation e a função start assume. Em seguida, ele aciona a funcionalidade de reprodução automática, permite o controle da roda, determina se estamos em um dispositivo de toque ou desktop e aguarda a primeira transição do slide dos títulos para adicionar a classe CSS apropriada.

Reprodução automática

Um dos principais recursos desse layout é o recurso de reprodução automática. Vamos ver a função correspondente:

 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); }

Primeiro, definimos o sinalizador de reprodução automática como verdadeiro, indicando que o controle deslizante está no modo de reprodução automática. Esse sinalizador é útil ao determinar se a reprodução automática deve ser acionada novamente após o usuário interagir com o controle deslizante. Em seguida, fazemos referência a todos os itens do controle deslizante (slides), pois alteraremos sua classe ativa e calcularemos o total de iterações que o controle deslizante terá adicionando todos os itens e dividindo por dois, pois temos dois layouts de controle deslizante sincronizados (principal e auxiliar) mas apenas um “slider” per se que altera os dois simultaneamente.

A parte mais interessante do código aqui é a função de loop. Ele invoca slideChange , fornecendo a direção do slide que veremos em um minuto, no entanto, a função de loop é chamada algumas vezes. Vamos ver por quê.

Se o argumento inicial for avaliado como verdadeiro, invocaremos a função de loop como um retorno de chamada requestAnimationFrame . Isso está acontecendo apenas na primeira carga do slider que aciona a mudança imediata do slide. Usando requestAnimationFrame , executamos o retorno de chamada fornecido antes da próxima repintura do quadro.

Diagrama das etapas usadas para criar o controle deslizante.

No entanto, como queremos continuar passando pelos slides no modo de reprodução automática, usaremos uma chamada repetida dessa mesma função. Isso geralmente é obtido com setInterval. Mas neste caso, usaremos uma das funções utilitárias – requestInterval . Enquanto setInterval funcionaria muito bem, requestInterval é um conceito avançado que se baseia em requestAnimationFrame e fornece uma abordagem de melhor desempenho. Ele garante que a função seja acionada novamente apenas se a guia do navegador estiver ativa.

Mais sobre esse conceito neste artigo incrível pode ser encontrado em truques de CSS. Observe que atribuímos o valor de retorno dessa função à nossa propriedade slider.handle . Esse ID exclusivo que a função retorna está disponível para nós e o usaremos para cancelar a reprodução automática mais tarde usando cancelAnimationFrame .

Mudança de slide

A função slideChange é a função principal em todo o conceito. Ele altera os slides, seja por reprodução automática ou por acionador do usuário. Ele está ciente da direção do slider, fornece looping para que, quando você chegar ao último slide, possa continuar para o primeiro slide. Aqui está como eu codifiquei:

 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 }); }

A ideia é determinar o slide ativo com base em seu índice de dados obtido do HTML. Vamos abordar cada etapa:

  1. Defina o sinalizador inativo do controle deslizante como falso. Isso indica que a mudança de slide está em andamento e os gestos de roda e toque estão desativados.
  2. A classe CSS de direção do slider anterior é redefinida e verificamos a nova. O parâmetro direction é fornecido por padrão como 'next' se estivermos vindo da função autoplay ou por uma função invocada pelo usuário wheelControl ou touchControl .
  3. Com base na direção, calculamos o índice do slide ativo e fornecemos a classe CSS da direção atual para o slider. Esta classe CSS é usada para determinar qual efeito de transição será usado (por exemplo, da direita para a esquerda ou da esquerda para a direita)
  4. Os slides obtêm suas classes CSS de “estado” (anterior, ativa) redefinidas usando outra função de utilitário que remove as classes CSS, mas pode ser invocada em um NodeList, em vez de apenas um único elemento DOM. Depois, apenas os slides anteriores e atualmente ativos recebem essas classes CSS adicionadas a eles. Isso permite que o CSS direcione apenas esses slides e forneça a transição adequada.
  5. setCurrent é um retorno de chamada que atualiza o indicador do controle deslizante com base no activeIndex.
  6. Por fim, esperamos que a transição do slide da imagem ativa termine para acionar o callback waitForIdle que reinicia a reprodução automática se tiver sido interrompido anteriormente pelo usuário.

Controles do usuário

Com base no tamanho da tela, adicionei dois tipos de controles de usuário: roda e toque. Controle de roda:

 const wheelControl = function () { slider.hero.addEventListener('wheel', e => { if (slider.idle) { const direction = e.deltaY > 0 ? 'next' : 'prev'; stopAutoplay(); changeSlide(direction); } }); }

Aqui, ouvimos wheel even e se o controle deslizante estiver atualmente em modo inativo (não animando uma mudança de slide no momento), determinamos a direção da roda, invocamos stopAutoplay para interromper a função de reprodução automática se estiver em andamento e alteramos o slide com base na direção. A função stopAutoplay nada mais é do que uma função simples que define nosso sinalizador de reprodução automática para o valor false e cancela nosso intervalo invocando a função utilitária cancelRequestInterval passando o identificador apropriado:

 const stopAutoplay = function () { slider.autoplay = false; utils().clearRequestInterval(slider.handle); }

Semelhante ao wheelControl , temos o touchControl que cuida dos gestos de toque:

 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); }

Ouvimos dois eventos: touchstart e touchmove . Em seguida, calculamos a diferença. Se retornar um valor negativo, mudamos para o próximo slide, pois o usuário passou da direita para a esquerda. Por outro lado, se o valor for positivo, o que significa que o usuário passou da esquerda para a direita, acionamos o slideChange com a direção passada como "anterior". Em ambos os casos, a funcionalidade de reprodução automática é interrompida.

Esta é uma implementação de gesto do usuário bastante simples. Para desenvolver isso, podemos adicionar botões anterior/próximo para acionar slideChange ao clicar ou adicionar uma lista com marcadores para ir diretamente a um slide com base em seu índice.

Conclusão e Considerações Finais sobre CSS

Então aqui está, uma maneira pura CSS/JS de codificar um layout de slider não padrão com efeitos de transição modernos.

Espero que você ache essa abordagem útil como uma maneira de pensar e possa usar algo semelhante em seus projetos front-end ao codificar um projeto que não foi necessariamente projetado de maneira convencional.

Para aqueles interessados ​​no efeito de transição de imagem, falarei sobre isso nas próximas linhas.

Se revisitarmos a estrutura HTML dos slides que forneci na seção de introdução, veremos que cada slide de imagem tem um div em torno dele com a classe CSS de abs-mask . O que essa div faz é ocultar uma parte da imagem visível em uma certa quantidade usando overflow:hidden e deslocando-a em uma direção diferente da imagem. Por exemplo, se observarmos a forma como o slide anterior está codificado:

 &.prev { z-index: 5; transform: translate3d(-100%, 0, 0); transition: 1s $easing; .abs-mask { transform: translateX(80%); transition: 1s $easing; } }

O slide anterior tem um deslocamento de -100% em seu eixo X, movendo-o para a esquerda do slide atual, no entanto, o div abs-mask interno é traduzido 80% para a direita, fornecendo uma janela de visualização mais estreita. Isso, em combinação com um índice z maior para o slide ativo, resulta em uma espécie de efeito de cobertura - a imagem ativa cobre a anterior e, ao mesmo tempo, estende sua área visível movendo a máscara que fornece a visualização completa.