Crea un dispositivo di scorrimento a pagina intera personalizzato con CSS e JavaScript

Pubblicato: 2022-03-11

Lavoro molto con layout personalizzati a schermo intero, praticamente quotidianamente. Di solito, questi layout implicano una notevole quantità di interazione e animazione. Che si tratti di una sequenza temporale complessa di transizioni innescata dal tempo o di un insieme di eventi guidato dall'utente basato sullo scorrimento, nella maggior parte dei casi l'interfaccia utente richiede molto di più del semplice utilizzo di una soluzione plug-in pronta all'uso con alcune modifiche e modifiche . D'altra parte, vedo che molti sviluppatori JavaScript tendono a raggiungere il loro plug-in JS preferito per semplificare il proprio lavoro, anche se l'attività potrebbe non richiedere tutti i campanelli e i fischietti forniti da un determinato plug-in.

Dichiarazione di non responsabilità: l'utilizzo di uno dei tanti plugin disponibili ha i suoi vantaggi, ovviamente. Avrai una varietà di opzioni che puoi utilizzare per adattare le tue esigenze senza dover fare molta codifica. Inoltre, la maggior parte degli autori di plugin ottimizza il proprio codice, lo rende compatibile cross-browser e multipiattaforma e così via. Tuttavia, ottieni una libreria a grandezza naturale inclusa nel tuo progetto forse solo per una o due cose diverse che fornisce. Non sto dicendo che l'utilizzo di un plug-in di terze parti di alcun tipo sia naturalmente una cosa negativa, lo faccio quotidianamente nei miei progetti, solo che generalmente è una buona idea valutare i pro e i contro di ogni approccio così com'è una buona pratica di codifica. Quando si tratta di fare le proprie cose in questo modo, richiede un po' più di conoscenza ed esperienza di programmazione per sapere cosa stai cercando, ma alla fine dovresti ottenere un pezzo di codice che faccia una cosa e una cosa solo in questo modo lo vuoi tu.

Questo articolo ha lo scopo di mostrare un approccio CSS/JS puro nello sviluppo di un layout di scorrimento attivato dallo scorrimento a schermo intero con animazione del contenuto personalizzata. In questo approccio ridotto, tratterò la struttura HTML di base che ti aspetteresti da un back-end CMS, le moderne tecniche di layout CSS (SCSS) e la codifica JavaScript vanilla per la piena interattività. Essendo semplice, questo concetto può essere facilmente esteso a un plug-in su larga scala e/o utilizzato in una varietà di applicazioni che non hanno dipendenze al centro.

Il design che creeremo è una vetrina minimalista del portfolio di un architetto con immagini e titoli in primo piano di ogni progetto. Lo slider completo con le animazioni sarà simile a questo:

Esempio di slider di un portfolio di architetti.

Puoi dare un'occhiata alla demo qui e puoi accedere al mio repository Github per ulteriori dettagli.

Panoramica HTML

Quindi ecco l'HTML di base con cui lavoreremo:

 <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 l'id di hero-slider è il nostro titolare principale. All'interno, il layout è suddiviso in sezioni:

  • Logo (sezione statica)
  • Presentazione su cui lavoreremo principalmente
  • Info (una sezione statica)
  • Slider nav che indicherà la diapositiva attualmente attiva e il numero totale di diapositive

Concentriamoci sulla sezione della presentazione poiché questo è il nostro punto di interesse in questo articolo. Qui abbiamo due parti: main e aux . Main è il div che contiene le immagini in primo piano mentre aux contiene i titoli delle immagini. La struttura di ogni diapositiva all'interno di questi due supporti è piuttosto semplice. Qui abbiamo una diapositiva dell'immagine all'interno del supporto principale:

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

L'attributo dei dati dell'indice è ciò che useremo per tenere traccia di dove ci troviamo nella presentazione. Il div maschera abs che useremo per creare un effetto di transizione interessante e il div immagine diapositiva contiene l'immagine specifica in primo piano. Le immagini vengono renderizzate in linea come se provenissero direttamente da un CMS e sono impostate dall'utente finale.

Allo stesso modo, il titolo scorre all'interno del supporto ausiliario:

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

Ogni titolo di diapositiva è un tag H2 con l'attributo di dati corrispondente e un collegamento per poter condurre alla singola pagina di quel progetto.

Anche il resto del nostro HTML è piuttosto semplice. Abbiamo un logo in alto, informazioni statiche che indicano all'utente in quale pagina si trova, una descrizione e un indicatore corrente/totale del dispositivo di scorrimento.

Panoramica CSS

Il codice CSS di origine è scritto in SCSS, un preprocessore CSS che viene quindi compilato in un normale CSS che il browser può interpretare. SCSS offre il vantaggio di utilizzare variabili, selezione nidificata, mixin e altre cose interessanti, ma deve essere compilato in CSS per consentire al browser di leggere il codice come dovrebbe. Ai fini di questo tutorial, ho utilizzato Scout-App per gestire la compilazione poiché volevo avere gli strumenti al minimo.

Ho usato flexbox per gestire il layout affiancato di base. L'idea è quella di avere la presentazione da un lato e la sezione informazioni dall'altro.

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

Entriamo nel posizionamento e, di nuovo, concentriamoci sulla sezione della presentazione:

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

Ho impostato il dispositivo di scorrimento principale in modo che sia posizionato in modo assoluto e le immagini di sfondo estendono l'intera area utilizzando la proprietà background-size: cover . Per fornire un maggiore contrasto con i titoli delle diapositive, ho impostato uno pseudoelemento assoluto che funge da sovrapposizione. Lo slider ausiliario contenente i titoli delle diapositive è posizionato nella parte inferiore dello schermo e sopra le immagini.

Poiché sarà visibile solo una diapositiva alla volta, ho impostato anche ogni titolo in modo che sia assoluto e ho calcolato la dimensione del supporto tramite JS per assicurarmi che non ci siano interruzioni, ma ne parleremo di più in una delle nostre prossime sezioni. Qui puoi vedere l'uso di una funzione SCSS chiamata estensione:

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

Dato che ho usato molto il posizionamento assoluto, ho inserito questo CSS in un estensibile per averlo facilmente disponibile in vari selettori. Inoltre, ho creato un mixin chiamato "delineato" per fornire un approccio DRY durante lo styling dei titoli e del titolo del cursore principale.

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

Per quanto riguarda la parte statica di questo layout, non ha nulla di complesso, ma qui puoi vedere un metodo interessante per posizionare il testo che deve essere sull'asse Y invece del suo normale flusso:

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

Vorrei attirare la vostra attenzione sulla proprietà transform-origin poiché l'ho trovata davvero sottoutilizzata per questo tipo di layout. Il modo in cui questo elemento è posizionato è che il suo ancoraggio rimane nell'angolo in alto a sinistra dell'elemento, impostando il punto di rotazione e facendo scorrere il testo continuamente da quel punto verso il basso senza problemi quando si tratta di dimensioni dello schermo diverse.

Diamo un'occhiata a una parte CSS più interessante: l'animazione di caricamento iniziale:

Carica l'animazione per il cursore.

Di solito, questo tipo di comportamento di animazione sincronizzato si ottiene utilizzando una libreria: GSAP, ad esempio, è uno dei migliori in circolazione, fornisce eccellenti capacità di rendering, è facile da usare e ha la funzionalità della sequenza temporale che consente allo sviluppatore di concatenare a livello di codice gli elementi transizioni l'una nell'altra.

Tuttavia, poiché questo è un puro esempio CSS/JS, ho deciso di andare davvero di base qui. Quindi ogni elemento è impostato sulla sua posizione iniziale per impostazione predefinita, nascosto dalla trasformazione o dall'opacità e mostrato al caricamento del cursore che viene attivato dal nostro JS. Tutte le proprietà di transizione vengono modificate manualmente per garantire un flusso naturale e interessante con ogni transizione che continua in un'altra fornendo una piacevole esperienza visiva.

 #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 c'è una cosa che vorrei che vedeste qui, è l'uso della proprietà transform . Quando si sposta un elemento HTML, sia esso una transizione o un'animazione, si consiglia di utilizzare la proprietà transform . Vedo molte persone che tendono a usare il margine o il riempimento o anche gli offset: in alto, a sinistra, ecc. Che non producono risultati adeguati quando si tratta di rendering.

Per ottenere una comprensione più approfondita di come utilizzare i CSS quando si aggiunge un comportamento interattivo, non potrei raccomandare abbastanza il seguente articolo.

È di Paul Lewis, un ingegnere di Chrome, e copre praticamente tutto ciò che si dovrebbe sapere sul rendering dei pixel nel Web, che si tratti di CSS o JS.

Panoramica di JavaScript e logica di scorrimento

Il file JavaScript è diviso in due funzioni distinte.

La funzione heroSlider che si occupa di tutte le funzionalità di cui abbiamo bisogno qui e la funzione utils in cui ho aggiunto diverse funzioni di utilità riutilizzabili. Ho commentato ciascuna di queste funzioni di utilità per fornire un contesto se stai cercando di riutilizzarle nel tuo progetto.

La funzione principale è codificata in modo da avere due rami: init e resize . Questi rami sono disponibili tramite il ritorno della funzione principale e vengono richiamati quando necessario. init è l'inizializzazione della funzione principale e viene attivata all'evento di caricamento della finestra. Allo stesso modo, il ramo di ridimensionamento viene attivato al ridimensionamento della finestra. L'unico scopo della funzione di ridimensionamento è ricalcolare la dimensione del cursore del titolo al ridimensionamento della finestra, poiché la dimensione del carattere del titolo può variare.

Nella funzione heroSlider , ho fornito un oggetto slider che contiene tutti i dati e i selettori di cui avremo bisogno:

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

Come nota a margine, questo approccio potrebbe essere facilmente adattato se, ad esempio, stai utilizzando React, poiché puoi archiviare i dati nello stato o utilizzare gli hook appena aggiunti. Per rimanere sul punto, esaminiamo semplicemente ciò che ciascuna delle coppie chiave-valore qui rappresenta:

  • Le prime quattro proprietà sono un riferimento HTML all'elemento DOM che manipoleremo.
  • La proprietà handle verrà utilizzata per avviare e interrompere la funzionalità di riproduzione automatica.
  • La proprietà idle è un flag che impedirà all'utente di forzare lo scorrimento mentre la diapositiva è in transizione.
  • activeIndex ci consentirà di tenere traccia della diapositiva attualmente attiva
  • interval indica l'intervallo di riproduzione automatica del dispositivo di scorrimento

Dopo l'inizializzazione dello slider, invochiamo due funzioni:

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

La funzione setHeight si rivolge a una funzione di utilità per impostare l'altezza del nostro dispositivo di scorrimento ausiliario in base alla dimensione massima del titolo. In questo modo ci assicuriamo che venga fornito un dimensionamento adeguato e che nessun titolo della diapositiva venga tagliato anche quando il suo contenuto scende in due righe.

La funzione loadingAnimation aggiunge una classe CSS all'elemento che fornisce le transizioni CSS introduttive:

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

Poiché il nostro indicatore di scorrimento è l'ultimo elemento nella timeline di transizione CSS, attendiamo la fine della sua transizione e invochiamo la funzione di avvio. Fornendo un parametro aggiuntivo come oggetto, ci assicuriamo che questo venga attivato solo una volta.

Diamo un'occhiata alla funzione di avvio:

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

Quindi, quando il layout è terminato, la sua transizione iniziale viene attivata dalla funzione loadingAnimation e la funzione di avvio prende il sopravvento. Quindi attiva la funzionalità di riproduzione automatica, abilita il controllo della rotellina, determina se ci troviamo su un dispositivo touch o desktop e attende la prima transizione della diapositiva dei titoli per aggiungere la classe CSS appropriata.

Riproduzione automatica

Una delle caratteristiche principali di questo layout è la funzione di riproduzione automatica. Esaminiamo la funzione corrispondente:

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

Innanzitutto, impostiamo il flag di riproduzione automatica su true, indicando che il dispositivo di scorrimento è in modalità di riproduzione automatica. Questo flag è utile per determinare se riattivare la riproduzione automatica dopo che l'utente ha interagito con il dispositivo di scorrimento. Quindi faremo riferimento a tutti gli elementi del dispositivo di scorrimento (diapositive), poiché cambieremo la loro classe attiva e calcoleremo le iterazioni totali che il dispositivo di scorrimento avrà sommando tutti gli elementi e dividendo per due poiché abbiamo due layout di scorrimento sincronizzati (principale e ausiliario) ma solo un "cursore" di per sé che li cambia entrambi contemporaneamente.

La parte più interessante del codice qui è la funzione loop. Invoca slideChange , fornendo la direzione della diapositiva che esamineremo tra un minuto, tuttavia, la funzione loop viene chiamata un paio di volte. Vediamo perché.

Se l'argomento iniziale viene valutato come true, invocheremo la funzione loop come callback requestAnimationFrame . Questo accade solo al primo caricamento del cursore che attiva il cambio diapositiva immediato. Usando requestAnimationFrame eseguiamo il callback fornito appena prima del successivo ridisegno del frame.

Schema dei passaggi utilizzati per creare lo slider.

Tuttavia, poiché vogliamo continuare a scorrere le diapositive in modalità di riproduzione automatica, utilizzeremo una chiamata ripetuta di questa stessa funzione. Questo di solito si ottiene con setInterval. Ma in questo caso, useremo una delle funzioni di utilità requestInterval . Mentre setInterval funzionerebbe bene, requestInterval è un concetto avanzato che si basa su requestAnimationFrame e fornisce un approccio più performante. Garantisce che la funzione venga riattivata solo se la scheda del browser è attiva.

Maggiori informazioni su questo concetto in questo fantastico articolo possono essere trovate sui trucchi CSS. Tieni presente che assegniamo il valore restituito da questa funzione alla nostra proprietà slider.handle . Questo ID univoco restituito dalla funzione è disponibile per noi e lo useremo per annullare la riproduzione automatica in seguito utilizzando cancelAnimationFrame .

Cambio diapositiva

La funzione slideChange è la funzione principale dell'intero concetto. Cambia le diapositive indipendentemente dalla riproduzione automatica o dal trigger dell'utente. È a conoscenza della direzione del cursore, fornisce un ciclo così quando arrivi all'ultima diapositiva sarai in grado di continuare con la prima diapositiva. Ecco come l'ho codificato:

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

L'idea è di determinare la diapositiva attiva in base al suo indice di dati ottenuto dall'HTML. Affrontiamo ogni passaggio:

  1. Imposta il flag di inattività del dispositivo di scorrimento su falso. Ciò indica che il cambio diapositiva è in corso e che i gesti della rotellina e del tocco sono disabilitati.
  2. La precedente classe CSS della direzione del cursore viene ripristinata e controlliamo quella nuova. Il parametro di direzione viene fornito per impostazione predefinita come "successivo" se proveniamo dalla funzione di riproduzione automatica o da una funzione richiamata dall'utente: wheelControl o touchControl .
  3. In base alla direzione, calcoliamo l'indice di diapositiva attiva e forniamo la classe CSS della direzione corrente allo slider. Questa classe CSS viene utilizzata per determinare quale effetto di transizione verrà utilizzato (ad es. da destra a sinistra o da sinistra a destra)
  4. Le diapositive ottengono il ripristino delle classi CSS di "stato" (precedente, attive) utilizzando un'altra funzione di utilità che rimuove le classi CSS ma può essere richiamata su un NodeList, anziché su un singolo elemento DOM. Successivamente, solo le diapositive precedenti e attualmente attive ottengono quelle classi CSS aggiunte ad esse. Ciò consente al CSS di indirizzare solo quelle diapositive e fornire una transizione adeguata.
  5. setCurrent è un callback che aggiorna l'indicatore del cursore in base all'activeIndex.
  6. Infine, attendiamo che la transizione della diapositiva dell'immagine attiva termini per attivare il callback waitForIdle che riavvia l'autoplay se è stato interrotto in precedenza dall'utente.

Controlli utente

In base alle dimensioni dello schermo, ho aggiunto due tipi di controlli utente: rotellina e tocco. Controllo delle ruote:

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

Qui, ascoltiamo la ruota anche e se il cursore è attualmente in modalità inattiva (non sta attualmente animando un cambio diapositiva) determiniamo la direzione della ruota, invochiamo stopAutoplay per interrompere la funzione di riproduzione automatica se è in corso e cambiamo la diapositiva in base alla direzione. La funzione stopAutoplay non è altro che una semplice funzione che imposta il nostro flag di riproduzione automatica sul valore false e annulla il nostro intervallo invocando la funzione di utilità cancelRequestInterval passandogli l'handle appropriato:

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

Simile a wheelControl , abbiamo touchControl che si occupa dei gesti tattili:

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

Ascoltiamo due eventi: touchstart e touchmove . Quindi, calcoliamo la differenza. Se restituisce un valore negativo, passiamo alla diapositiva successiva mentre l'utente ha fatto scorrere il dito da destra a sinistra. D'altra parte, se il valore è positivo, a significare che l'utente ha fatto scorrere il dito da sinistra a destra, slideChange con la direzione passata come "precedente". In entrambi i casi, la funzionalità di riproduzione automatica viene interrotta.

Questa è un'implementazione di gesti dell'utente piuttosto semplice. Per basarci su questo, potremmo aggiungere pulsanti precedente/successivo per attivare slideChange al clic o aggiungere un elenco puntato per andare direttamente a una diapositiva in base al suo indice.

Conclusione e considerazioni finali sui CSS

Quindi ecco qua, un puro modo CSS/JS di codificare un layout di slider non standard con effetti di transizione moderni.

Spero che tu possa trovare questo approccio utile come modo di pensare e che tu possa usare qualcosa di simile nei tuoi progetti front-end durante la codifica di un progetto che non è stato necessariamente progettato in modo convenzionale.

Per quelli di voi interessati all'effetto di transizione dell'immagine, ne parlerò nelle prossime righe.

Se rivisitiamo la struttura HTML delle diapositive che ho fornito nella sezione introduttiva, vedremo che ogni diapositiva dell'immagine ha un div attorno ad essa con la classe CSS di abs-mask . Quello che fa questo div è che nasconde una parte dell'immagine visibile di una certa quantità usando overflow:hidden e spostandola in una direzione diversa dall'immagine. Ad esempio, se osserviamo il modo in cui è codificata la diapositiva precedente:

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

La diapositiva precedente ha un offset -100% nel suo asse X, spostandola a sinistra della diapositiva corrente, tuttavia, il div abs-mask interna viene traslato dell'80% a destra, fornendo una vista più stretta. Questo, in combinazione con un indice z più grande per la diapositiva attiva, si traduce in una sorta di effetto di copertura: l'immagine attiva copre la precedente estendendo allo stesso tempo la sua area visibile spostando la maschera che fornisce la vista completa.