Costruire una linea temporale orizzontale con CSS e Javascript
Italian (Italiano) translation by Cinzia Sgariglia (you can also view the original English article)
In un articolo precedente, vi ho mostrato come costruire una linea temporale verticale adattiva da zero. Oggi, tratterò il processo di creazione della relativa linea temporale orizzontale.
Come di consueto, per avere un'idea iniziale di ciò che andremo a costruire, date un'occhiata alla demo associata su CodePen (date uno sguardo alla versione più grande per un'esperienza migliore):
Abbiamo molto da trattare, quindi iniziamo!
1. Il markup HTML
Il markup è identico al markup che abbiamo descritto per la linea temporale verticale, eccetto che tre piccole cose:
- Usiamo una lista ordinata invece di una lista non ordinata poiché è più corretto semanticamente.
- C'è un elemento extra nella lista (l'ultimo) che è vuoto. In una prossima sezione, discuteremo la ragione.
- C'è un elemento extra (cioè
arrows
) che è responsabile della navigazione della linea temporale.
Ecco il markup richiesto:
<section class="timeline"> <ol> <li> <div> <time>1934</time> Some content here </div> </li> <!-- more list items here --> <li></li> </ol> <div class="arrows"> <button class="arrow arrow__prev disabled" disabled> <img src="arrow_prev.svg" alt="prev timeline arrow"> </button> <button class="arrow arrow__next"> <img src="arrow_next.svg" alt="next timeline arrow"> </button> </div> </section>
Lo stato iniziale della linea temporale ha questo aspetto:
2. Aggiungere gli stili CSS iniziali
Dopo alcuni stili di base per i font, gli stili per i colori, ecc. che abbiamo omesso per motivi di semplicità, specifichiamo alcune regole CSS strutturali:
.timeline { white-space: nowrap; overflow-x: hidden; } .timeline ol { font-size: 0; width: 100vw; padding: 250px 0; transition: all 1s; } .timeline ol li { position: relative; display: inline-block; list-style-type: none; width: 160px; height: 3px; background: #fff; } .timeline ol li:last-child { width: 280px; } .timeline ol li:not(:first-child) { margin-left: 14px; } .timeline ol li:not(:last-child)::after { content: ''; position: absolute; top: 50%; left: calc(100% + 1px); bottom: 0; width: 12px; height: 12px; transform: translateY(-50%); border-radius: 50%; background: #F45B69; }
La cosa più importante qui è che noterete due cose:
- Assegniamo un padding grande alla lista in alto e in basso. Ancora, spiegheremo perché succede nella prossima sezione.
- Come noterete nella demo seguente, a questo punto non possiamo vedere tutti gli elementi della lista perché la lista ha
width: 100vw
e il suo genitore haoverflow-x:hidden
. Questo di fatto "maschera" gli elementi della lista. Grazie alla navigazione della linea temporale, tuttavia, saremo in grado di navigare più tardi tra gli elementi.
Con queste regole, ecco lo stato attuale della linea temporale (senza alcun contenuto reale, per mantenere le cose chiare):
3. Gli stile degli elementi della linea temporale
A questo punto daremo uno stile agli elementi div
(li chiameremo "elementi della linea temporale" d'ora in poi) che sono parte degli elementi della lista così come i loro pseudo-elementi ::before
.
Inoltre, useremo le pseudo-classi :nth-child(odd)
e :nth-child(even)
per differenziare gli stili dei div dispari e pari.
Ecco gli stili comuni per gli elementi della linea temporale:
.timeline ol li div { position: absolute; left: calc(100% + 7px); width: 280px; padding: 15px; font-size: 1rem; white-space: normal; color: black; background: white; } .timeline ol li div::before { content: ''; position: absolute; top: 100%; left: 0; width: 0; height: 0; border-style: solid; }
Poi alcuni stili per quelli dispari:
.timeline ol li:nth-child(odd) div { top: -16px; transform: translateY(-100%); } .timeline ol li:nth-child(odd) div::before { top: 100%; border-width: 8px 8px 0 0; border-color: white transparent transparent transparent; }
E alla fine alcuni stili per quelli pari:
.timeline ol li:nth-child(even) div { top: calc(100% + 16px); } .timeline ol li:nth-child(even) div::before { top: -8px; border-width: 8px 0 0 8px; border-color: transparent transparent transparent white; }
Ecco il nuovo stato della linea temporale con il contenuto aggiunto di nuovo:
Come avete probabilmente notato, gli elementi della linea temporale sono posizionati in modo assoluto. Ciò significa che sono rimossi dal normale flusso del documento. Con questo in mente, al fine di garantire che l'intera linea temporale appaia, dobbiamo impostare i valori del padding in alto e in basso grandi per la lista. Se non applichiamo alcun padding, la linea temporale verrà tagliata:



4. Gli stili della navigazione della linea temporale
Ora è il momento di creare lo stile dei bottoni della navigazione. Ricordate che come impostazione predefinita disabilitiamo la freccia precedente e gli diamo la classe disabled
.
Ecco gli stili CSS associati:
.timeline .arrows { display: flex; justify-content: center; margin-bottom: 20px; } .timeline .arrows .arrow__prev { margin-right: 20px; } .timeline .disabled { opacity: .5; } .timeline .arrows img { width: 45px; height: 45px; }
Le regole sopra ci danno questa linea temporale:
5. Aggiungere l'interattività
La struttura di base della linea temporale è pronta. Aggiungiamole dell'interattività!
Variabili
Prima le cose importanti, impostiamo un bel po' di variabili che useremo dopo.
const timeline = document.querySelector(".timeline ol"), elH = document.querySelectorAll(".timeline li > div"), arrows = document.querySelectorAll(".timeline .arrows .arrow"), arrowPrev = document.querySelector(".timeline .arrows .arrow__prev"), arrowNext = document.querySelector(".timeline .arrows .arrow__next"), firstItem = document.querySelector(".timeline li:first-child"), lastItem = document.querySelector(".timeline li:last-child"), xScrolling = 280, disabledClass = "disabled";
Inizializzare le cose
Quando tutte le risorse della pagina sono pronte, viene chiamata la funzione init
.
window.addEventListener("load", init);
Questa funzione attiva quattro sotto-funzioni:
function init() { setEqualHeights(elH); animateTl(xScrolling, arrows, timeline); setSwipeFn(timeline, arrowPrev, arrowNext); setKeyboardFn(arrowPrev, arrowNext); }
Come vedremo tra un momento, ognuna di queste funzioni compie un certo compito.
Gli elementi della linea temporale a pari altezza
Se tornate indietro all'ultima demo, noterete che gli elementi della linea temporale non hanno pari altezze. Questo non incide sulla funzionalità principale della nostra linea temporale, ma potreste preferirla se tutti gli elementi avessero la stessa altezza. Per ottenerlo, possiamo dargli sia un'altezza fissa tramite CSS (soluzione facile) o un'altezza dinamica che corrisponde all'altezza dell'elemento più alto tramite Javascript.
La seconda opzione è più flessibile e stabile, quindi ecco una funzione che implementa questo comportamento:
function setEqualHeights(el) { let counter = 0; for (let i = 0; i < el.length; i++) { const singleHeight = el[i].offsetHeight; if (counter < singleHeight) { counter = singleHeight; } } for (let i = 0; i < el.length; i++) { el[i].style.height = `${counter}px`; } }
Questa funzione recupera l'altezza dell'elemento più alto e lo imposta come altezza predefinita per tutti gli elementi.
Ecco come appare nella demo:
6. Animare le linea temporale
Adesso focalizziamoci sull'animazione della linea temporale. Costruiremo la funzione che implementa questo comportamento passo passo.
Primo, registriamo un listener dell'evento clic per i bottoni della linea temporale:
function animateTl(scrolling, el, tl) { for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // code here }); } }
Ogni volta che un bottone viene cliccato, controlliamo lo stato disabilitato dei bottoni della linea temporale e se non sono disabilitati, li disabilitiamo. Ciò assicura che entrambi i bottoni saranno cliccati solo una volta fino a che l'animazione finisce.
Quindi, in termini di codice, il gestore del clic inizialmente contiene queste linee:
if (!arrowPrev.disabled) { arrowPrev.disabled = true; } if (!arrowNext.disabled) { arrowNext.disabled = true; }
I prossimi passi sono i seguenti:
- Controlliamo per vedere se è la prima volta che abbiamo cliccato su un bottone. Di nuovo, teniamo in mente che il bottone precedente sia disabilitato in modo predefinito, così il solo bottone che può essere cliccato inizialmente è quello successivo.
- Se invece è la prima volta, usiamo la proprietà
transform
per muovere la linea temporale 280px a destra. Il valore della variabilexScrolling
determina l'ammontare del movimento. - Al contrario, se abbiamo già cliccato su un bottone, ritroviamo il valore attuale di
transform
della linea temporale e aggiungiamo o rimuoviamo a quel valore l'ammontare desiderato del movimento (cioè 280px). Quindi, fintanto che clicchiamo sul bottone precedente, il valore della proprietàtransform
diminuisce e la linea temporale viene spostata da sinistra a destra. Tuttavia, quando il bottone successivo viene cliccato, il valore della proprietàtransform
aumenta e la linea temporale viene spostata da destra a sinistra.
Il codice che implementa questa funzionalità è il seguente:
let counter = 0; for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here const sign = (this.classList.contains("arrow__prev")) ? "" : "-"; if (counter === 0) { tl.style.transform = `translateX(-${scrolling}px)`; } else { const tlStyle = getComputedStyle(tl); // add more browser prefixes if needed here const tlTransform = tlStyle.getPropertyValue("-webkit-transform") || tlStyle.getPropertyValue("transform"); const values = parseInt(tlTransform.split(",")[4]) + parseInt(`${sign}${scrolling}`); tl.style.transform = `translateX(${values}px)`; } counter++; }); }
Bel lavoro! Abbiamo appena definito un modo per animare la linea temporale. La prossima sfida è di capire quando questa animazione dovrebbe fermarsi. Ecco il nostro approccio:
- Quando il primo elemento della linea temporale diventa totalmente visibile, significa che abbiamo già raggiunto l'inizio della linea temporale e perciò disabilitiamo il bottone precedente. Ci assicuriamo anche che il bottone successivo sia abilitato.
- Quando l'ultimo elemento diventa totalmente visibile, significa che abbiamo già raggiunto la fine della linea temporale e perciò disabilitiamo il bottone successivo. Pertanto, ci assicuriamo anche il bottone precedente sia abilitato.
Ricordate che l'ultimo elemento è vuoto con larghezza uguale alla larghezza degli elementi della linea temporale (cioè 280px). Gli diamo questo valore (o un valore più alto) perché vogliamo essere sicuri che l'ultimo elemento della linea temporale sarà visibile prima di disabilitare il bottone successivo.
Per rilevare se determinati elementi sono o meno totalmente visibili nell'area visibile attuale, approfitteremo dello stesso codice che abbiamo usato per la linea temporale verticale. Il codice richiesto che proviene da questa conversazione su Stack Overflow è il seguente:
function isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }
Oltre alla funzione sopra, definiamo un altro helper:
function setBtnState(el, flag = true) { if (flag) { el.classList.add(disabledClass); } else { if (el.classList.contains(disabledClass)) { el.classList.remove(disabledClass); } el.disabled = false; } }
Questa funzione aggiunge o rimuove la classe disabled
da un elemento basata sul valore del parametro flag
. In più, può cambiare lo stato di disabilitato da questo elemento.
Dato quello che abbiamo descritto sopra, ecco il codice che definiamo per controllare se l'animazione dovrebbe fermarsi o no:
for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here // code for stopping the animation setTimeout(() => { isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false); isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false); }, 1100); // other code here }); }
Notate che c'è 1.1 secondo di ritardo prima di eseguire questo codice. Perché accade questo?
Se torniamo al nostro CSS, vedremo questa regola:
.timeline ol { transition: all 1s; }
Quindi, l'animazione della linea temporale ha bisogno di 1 secondo per completarsi. Finché si completa, aspettiamo 100 millisecondi e poi effettuiamo i nostri controlli.
Ecco la linea temporale con le animazioni:
7. Aggiungere il supporto per swipe
Finora, la linea temporale non risponde agli eventi touch. Però sarebbe bello se potessimo aggiungere questa funzionalità. Per realizzarla, possiamo scrivere la nostra implementazione Javascript o usare una delle librerie collegate (per es. Hammer.js, TouchSwipe.js) che esistono in rete.
Per la nostra dimostrazione, semplificheremo e useremo Hammer.js, quindi per prima cosa, inseriamo questa libreria nel nostro CodePen:



Poi dichiariamo la funzione associata:
function setSwipeFn(tl, prev, next) { const hammer = new Hammer(tl); hammer.on("swipeleft", () => next.click()); hammer.on("swiperight", () => prev.click()); }
All'interno della funzione sopra, procediamo come segue:
- Create un'istanza di Hammer.
- Registrate gli handler per gli eventi
swipeleft
eswiperight
. - Quando strisciamo sulla linea temporale nella direzione sinistra, provochiamo un clic sul bottone successivo e quindi la linea temporale è animata da destra a sinistra.
- Quando strisciamo sulla linea temporale nella direzione destra, provochiamo un clic sul bottone precedente e quindi la linea temporale è animata da sinistra a destra.
La linea temporale con il supporto per swipe:
Aggiungere la navigazione da tastiera
Aumentiamo ulteriormente l'esperienza utente fornendo il supporto per la navigazione dalla tastiera. I nostri obiettivi:
- Quando vengono premuti il tasto freccia sinistra o destra, il documento dovrebbe scorrere dalla posizione in alto della linea temporale (se un'altra sezione della pagina è attualmente visibile). Ciò assicura che l'intera linea temporale sarà visibile.
- Nello specifico, quando viene premuto il tasto freccia sinistra, la linea temporale dovrebbe essere animata da sinistra verso destra.
- Allo stesso modo, quando viene premuto il tasto freccia destra, la linea temporale dovrebbe essere animata da destra verso sinistra.
La funzione associata è la seguente:
function setKeyboardFn(prev, next) { document.addEventListener("keydown", (e) => { if ((e.which === 37) || (e.which === 39)) { const timelineOfTop = timeline.offsetTop; const y = window.pageYOffset; if (timelineOfTop !== y) { window.scrollTo(0, timelineOfTop); } if (e.which === 37) { prev.click(); } else if (e.which === 39) { next.click(); } } }); }
La linea temporale con il supporto per la tastiera:
8. Rendere responsive
Abbiamo quasi fatto! Ultimo ma non meno importante, rendiamo la linea temporale responsive. Quando il viewport è meno di 600px, dovrebbe avere il seguente disposizione del layout:



Poiché stiamo usando l'approccio prima il desktop, ecco le regole CSS che dobbiamo sovrascrivere:
@media screen and (max-width: 599px) { .timeline ol, .timeline ol li { width: auto; } .timeline ol { padding: 0; transform: none !important; } .timeline ol li { display: block; height: auto; background: transparent; } .timeline ol li:first-child { margin-top: 25px; } .timeline ol li:not(:first-child) { margin-left: auto; } .timeline ol li div { width: 94%; height: auto !important; margin: 0 auto 25px; } .timeline ol li:nth-child div { position: static; } .timeline ol li:nth-child(odd) div { transform: none; } .timeline ol li:nth-child(odd) div::before, .timeline ol li:nth-child(even) div::before { left: 50%; top: 100%; transform: translateX(-50%); border: none; border-left: 1px solid white; height: 25px; } .timeline ol li:last-child, .timeline ol li:nth-last-child(2) div::before, .timeline ol li:not(:last-child)::after, .timeline .arrows { display: none; } }
Nota: Per due delle regole sopra, abbiamo dovuto usare la regola !important
per superare gli stili inline collegati applicati tramite Javascript.
Lo stato finale della linea temporale:
Il supporto browser
la dimostrazione funziona bene in tutti i browser e dispositivi recenti. Inoltre, come forse avete notato, usiamo Babel per compilare il nostro codice ES6 fino a ES5.
L'unico piccolo problema che ho incontrato durante la verifica è il cambiamento di rendering del testo quando la linea temporale viene animata. Sebbene abbia provato vari approcci proposti su diverse conversazioni su Stack Overflow, non ho trovato una soluzione chiara per tutti i sistemi operativi e i browser. Quindi, ricordate che potreste vedere piccoli problemi di rendering dei caratteri non appena la linea temporale viene animata.
Conclusioni
In questo tutorial abbastanza sostanzioso, abbiamo iniziato con una semplice lista ordinata e abbiamo creato una linea temporale orizzontale responsive. Senza dubbio, abbiamo coperto molte cose interessanti, ma spero che vi siate divertiti a lavorare verso il risultato finale e che vi abbia aiutato a guadagnare delle nuove conoscenze.
Se avete delle domande o se c'è qualcosa che non avete capito, fatemelo sapere nei commenti qui sotto!
Prossimi passi
Se volete migliorare ulteriormente o estendere questa linea temporale, ecco alcune cose che potete fare:
- Aggiungere il supporto per il trascinamento. Invece di cliccare i bottoni della linea temporale per navigare, potreste solo trascinare l'area della linea temporale. Per questo comportamento, potreste usare sia l'API Drag and Drop nativa (la quale per sfortuna non supporta i dispositivi mobili in questo momento) oppure una libreria esterna come Draggable.js.
- Migliorare il comportamento della linea temporale non appena ridimensioniamo la finestra del browser. Per esempio, non appena ridimensioniamo la finestra, i bottoni dovrebbero essere abilitati e disabilitati di conseguenza.
- Organizzare il codice in un modo più gestibile. Forse, usare un normale Javascript Design Pattern.