Timeline Verticale Animata con CSS: Tutorial Completo

Timeline Verticale Animata con CSS: Tutorial Completo

Perché una timeline verticale CSS nel 2026

Le timeline verticali sono uno dei pattern UI più efficaci per mostrare cronologie, processi step-by-step, roadmap di prodotto o curriculum vitae. Nel 2026, con il supporto stabile delle scroll-driven animations in tutti i browser moderni e la maturità delle CSS custom properties, costruire una timeline completamente animata senza una riga di JavaScript è finalmente possibile — e conveniente.

Il vantaggio principale di un approccio CSS-first è la performance: le animazioni gestite dal browser tramite compositor thread non bloccano il main thread, risultando in 60fps stabili anche su dispositivi mobili entry-level. Se stai già lavorando su ottimizzazione delle prestazioni, dai un’occhiata alla guida Web Performance 2026: Core Web Vitals e ottimizzazioni chiave per capire come le animazioni impattano sui tuoi score.

In questo tutorial costruiremo una timeline verticale completa: struttura HTML semantica, stile con CSS moderno, animazioni al caricamento con @keyframes, effetti hover, e — ciliegina sulla torta — animazioni legate allo scroll con animation-timeline: scroll(). Tutto il codice è production-ready e copia-incollabile.

Struttura HTML semantica della timeline

Partiamo dalle fondamenta. Una timeline è semanticamente una lista ordinata di eventi: ha senso usare <ol> se l’ordine cronologico è importante, oppure <ul> se gli elementi sono paralleli. Ogni item contiene un marker visivo (il pallino sulla linea) e un content block con data e testo.

<section class="timeline" aria-label="Cronologia eventi">
  <ol class="timeline__list">

    <li class="timeline__item">
      <div class="timeline__marker" aria-hidden="true"></div>
      <div class="timeline__content">
        <time class="timeline__date" datetime="2024-01">Gennaio 2024</time>
        <h3 class="timeline__title">Lancio del progetto</h3>
        <p class="timeline__text">
          Prima release pubblica con le feature core implementate
          e il design system consolidato.
        </p>
      </div>
    </li>

    <li class="timeline__item">
      <div class="timeline__marker" aria-hidden="true"></div>
      <div class="timeline__content">
        <time class="timeline__date" datetime="2024-06">Giugno 2024</time>
        <h3 class="timeline__title">v2.0 - Nuove integrazioni</h3>
        <p class="timeline__text">
          Aggiunta API REST, dashboard analitica
          e supporto multi-tenant.
        </p>
      </div>
    </li>

    <li class="timeline__item timeline__item--active">
      <div class="timeline__marker" aria-hidden="true"></div>
      <div class="timeline__content">
        <time class="timeline__date" datetime="2025-01">Gennaio 2025</time>
        <h3 class="timeline__title">v3.0 - AI Features</h3>
        <p class="timeline__text">
          Integrazione modelli AI, automazione workflow
          e dashboard predittiva.
        </p>
      </div>
    </li>

  </ol>
</section>

Nota l’uso di <time datetime="..."> per le date: migliora sia la semantica per i motori di ricerca che l’accessibilità per i lettori di schermo. L’attributo aria-hidden="true" sul marker evita che venga letto inutilmente. Se vuoi approfondire le best practice HTML per l’accessibilità, la guida Accessibilità web 2026: guida completa per developer è un ottimo punto di partenza.

CSS base: linea verticale e marker

Il cuore visivo della timeline è la linea verticale continua che collega tutti gli item. La tecnica più pulita usa uno pseudo-elemento ::before sull’<ol>, posizionato in modo assoluto e centrato rispetto ai marker. I marker stessi sfruttano position: absolute per uscire dal flusso senza rompere il layout dei contenuti.

/* ── Variabili CSS ── */
.timeline {
  --tl-accent:       #06b6d4;
  --tl-accent-light: #67e8f9;
  --tl-bg:           #0f172a;
  --tl-card-bg:      #1e293b;
  --tl-text:         #e2e8f0;
  --tl-muted:        #94a3b8;
  --tl-line-width:   2px;
  --tl-marker-size:  16px;
  --tl-gap:          2.5rem;
  --tl-offset:       1.5rem; /* distanza marker dal bordo sinistro */
}

/* ── Lista ── */
.timeline__list {
  position: relative;
  list-style: none;
  padding: 0 0 0 calc(var(--tl-offset) * 2 + var(--tl-marker-size));
  margin: 0;
}

/* Linea verticale tramite pseudo-elemento */
.timeline__list::before {
  content: "";
  position: absolute;
  left: calc(var(--tl-offset) + var(--tl-marker-size) / 2 - var(--tl-line-width) / 2);
  top: 0;
  bottom: 0;
  width: var(--tl-line-width);
  background: linear-gradient(
    to bottom,
    transparent,
    var(--tl-accent) 8%,
    var(--tl-accent) 92%,
    transparent
  );
}

/* ── Item ── */
.timeline__item {
  position: relative;
  padding-bottom: var(--tl-gap);
}

/* ── Marker (pallino) ── */
.timeline__marker {
  position: absolute;
  left: calc(-1 * var(--tl-offset) - var(--tl-marker-size));
  top: 0.25rem;
  width: var(--tl-marker-size);
  height: var(--tl-marker-size);
  border-radius: 50%;
  background: var(--tl-bg);
  border: var(--tl-line-width) solid var(--tl-accent);
  transition: background 0.3s ease, box-shadow 0.3s ease;
}

/* Stato attivo */
.timeline__item--active .timeline__marker {
  background: var(--tl-accent);
  box-shadow: 0 0 0 4px rgba(6, 182, 212, 0.2);
}

/* ── Content card ── */
.timeline__content {
  background: var(--tl-card-bg);
  border: 1px solid rgba(6, 182, 212, 0.15);
  border-radius: 0.75rem;
  padding: 1.25rem 1.5rem;
}

.timeline__date {
  display: block;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--tl-accent);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin-bottom: 0.4rem;
}

.timeline__title {
  font-size: 1.125rem;
  font-weight: 700;
  color: var(--tl-text);
  margin: 0 0 0.5rem;
}

.timeline__text {
  font-size: 0.9375rem;
  color: var(--tl-muted);
  line-height: 1.6;
  margin: 0;
}

L’uso massiccio di CSS custom properties non è solo una questione estetica: permette di creare varianti di colore o dimensione semplicemente sovrascrivendo le variabili su un singolo elemento padre, senza duplicare regole. Questo è lo stesso principio dei temi dinamici approfondito in Sfruttare le variabili CSS per temi adattivi in tempo reale.

Animazioni al caricamento con @keyframes

La prima animazione da implementare è quella di ingresso: ogni item appare con un leggero fade + slide laterale quando la pagina carica. Usiamo @keyframes combinati con animation-delay scalato via custom property per creare un effetto stagger pulito.

/* ── Keyframes ingresso ── */
@keyframes tl-slide-in {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes tl-marker-pop {
  0%   { transform: scale(0); opacity: 0; }
  70%  { transform: scale(1.3); }
  100% { transform: scale(1); opacity: 1; }
}

/* ── Assegna indice agli item tramite :nth-child ── */
.timeline__item:nth-child(1) { --tl-index: 0; }
.timeline__item:nth-child(2) { --tl-index: 1; }
.timeline__item:nth-child(3) { --tl-index: 2; }
.timeline__item:nth-child(4) { --tl-index: 3; }
.timeline__item:nth-child(5) { --tl-index: 4; }
/* Per liste lunghe, usa JS: item.style.setProperty('--tl-index', i) */

/* ── Animazione content ── */
.timeline__content {
  animation: tl-slide-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--tl-index, 0) * 120ms + 200ms);
}

/* ── Animazione marker ── */
.timeline__marker {
  animation: tl-marker-pop 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--tl-index, 0) * 120ms + 100ms);
}

/* ── Rispetta prefers-reduced-motion ── */
@media (prefers-reduced-motion: reduce) {
  .timeline__content,
  .timeline__marker {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Il media query prefers-reduced-motion è non opzionale: è una questione di accessibilità per utenti con disturbi vestibolari. Nota l’uso di animation-fill-mode: both (shorthand via both) che mantiene lo stato iniziale prima che l’animazione parta, evitando flash di contenuto non stilizzato.

Effetti hover interattivi

Gli effetti hover trasformano la timeline da elemento statico a componente interattivo. L’obiettivo è fornire feedback visivo chiaro senza essere invasivi. Aggiungiamo un bordo illuminato sulla card, un’espansione del marker e un’indicazione visiva con ::before.

/* ── Hover sull'item ── */
.timeline__item {
  cursor: default;
}

/* Highlight della card */
.timeline__item:hover .timeline__content {
  border-color: rgba(6, 182, 212, 0.5);
  box-shadow:
    0 0 0 1px rgba(6, 182, 212, 0.1),
    0 4px 24px rgba(6, 182, 212, 0.08);
  transform: translateX(4px);
  transition:
    border-color 0.25s ease,
    box-shadow    0.25s ease,
    transform     0.25s ease;
}

/* Marker pulse on hover */
.timeline__item:hover .timeline__marker {
  background: var(--tl-accent);
  box-shadow:
    0 0 0 4px rgba(6, 182, 212, 0.15),
    0 0 12px rgba(6, 182, 212, 0.3);
}

/* Connettore orizzontale marker -> card */
.timeline__item::after {
  content: "";
  position: absolute;
  left: calc(-1 * var(--tl-offset) + var(--tl-marker-size) / 2);
  top: calc(0.25rem + var(--tl-marker-size) / 2 - 1px);
  width: calc(var(--tl-offset) - var(--tl-marker-size) / 2);
  height: 2px;
  background: rgba(6, 182, 212, 0.2);
  transition: background 0.25s ease;
}

.timeline__item:hover::after {
  background: rgba(6, 182, 212, 0.6);
}

/* Titolo highlight on hover */
.timeline__item:hover .timeline__title {
  color: var(--tl-accent-light);
  transition: color 0.2s ease;
}

La transizione su transform: translateX(4px) crea un leggero effetto di “avanzamento” della card che guida visivamente l’utente. Combinato con il cambio di bordo e ombra, trasmette chiaramente l’interattività dell’elemento — un principio fondamentale delle transizioni CSS fluide.

Scroll-driven animations: la linea che cresce

La feature più spettacolare è la linea verticale che cresce man mano che l’utente scrolla. Con le scroll-driven animations CSS (supportate da Chrome 115+, Firefox 110+, Safari 18+) possiamo collegare direttamente il progresso di un’animazione alla posizione di scroll — zero JavaScript richiesto.

/* ── Linea di progresso scroll-driven ── */

/* La linea principale viene sostituita da una versione animata */
.timeline__list::before {
  /* Override del background precedente */
  background: rgba(6, 182, 212, 0.15); /* traccia leggera sempre visibile */
}

/* Pseudo-elemento aggiuntivo per la linea di progresso */
.timeline__list::after {
  content: "";
  position: absolute;
  left: calc(var(--tl-offset) + var(--tl-marker-size) / 2 - var(--tl-line-width) / 2);
  top: 0;
  width: var(--tl-line-width);
  background: linear-gradient(
    to bottom,
    var(--tl-accent),
    var(--tl-accent-light)
  );
  box-shadow: 0 0 8px rgba(6, 182, 212, 0.4);

  /* Altezza animata tramite scroll */
  transform-origin: top center;
  scale: 1 0; /* inizia a 0 in altezza */
  animation: tl-line-grow linear both;

  /* Collega all'elemento timeline stesso */
  animation-timeline: view();
  animation-range: entry 0% exit 100%;
}

@keyframes tl-line-grow {
  from { scale: 1 0; }
  to   { scale: 1 1; }
}

/* ── Fade-in item al passaggio nella viewport ── */
.timeline__item {
  /* Reset animazione precedente per supporto scroll-driven */
  animation: tl-item-reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}

@keyframes tl-item-reveal {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Nota: quando si usano scroll-driven animations sugli item,
   rimuovere le animazioni @keyframes di ingresso precedenti
   per evitare conflitti */

/* ── Fallback per browser senza supporto ── */
@supports not (animation-timeline: scroll()) {
  .timeline__item {
    animation: tl-slide-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
    animation-delay: calc(var(--tl-index, 0) * 120ms + 200ms);
  }
  .timeline__list::after {
    display: none;
  }
}

Le scroll-driven animations sono trattate in dettaglio nell’articolo Animazioni legate allo scroll: view-timeline e animation-timeline, che consiglio di leggere per capire tutta la potenza di questa API. Il blocco @supports garantisce un graceful degradation per i browser più datati.

Variante alternata: item sinistra/destra

Per layout desktop più elaborati, una timeline può alternare gli item sui due lati della linea centrale. La tecnica usa :nth-child(even) per invertire il layout su schermi larghi, con un fallback lineare su mobile.

/* ── Timeline alternata (solo da 768px) ── */
@media (min-width: 768px) {
  .timeline--alternate .timeline__list {
    padding: 0; /* rimuove padding unilaterale */
  }

  /* Linea centrata */
  .timeline--alternate .timeline__list::before,
  .timeline--alternate .timeline__list::after {
    left: 50%;
    transform: translateX(-50%);
  }

  /* Item a sinistra di default */
  .timeline--alternate .timeline__item {
    width: calc(50% - 2rem);
    margin-left: 0;
  }

  /* Item pari a destra */
  .timeline--alternate .timeline__item:nth-child(even) {
    margin-left: auto;
    margin-right: 0;
  }

  /* Marker centrato sulla linea */
  .timeline--alternate .timeline__marker {
    left: auto;
    right: calc(-2rem - var(--tl-marker-size) / 2);
  }

  .timeline--alternate .timeline__item:nth-child(even) .timeline__marker {
    right: auto;
    left: calc(-2rem - var(--tl-marker-size) / 2);
  }

  /* Card entry animation da direzione corretta */
  .timeline--alternate .timeline__item:nth-child(even) .timeline__content {
    animation-name: tl-slide-in-right;
  }

  @keyframes tl-slide-in-right {
    from {
      opacity: 0;
      transform: translateX(20px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }
}

Per aggiungere la classe timeline--alternate solo su schermi larghi, puoi usare una riga di JavaScript leggera o, ancora meglio, un container query se la timeline è un componente riutilizzabile in contesti diversi. I container queries sono perfetti per questo caso d’uso — trovi tutti i dettagli in Container Queries: best practice responsive.

FAQ e Domande Frequenti

Posso usare questa timeline con React o Vue senza modifiche?

Sì, il CSS è completamente framework-agnostic. In React basterà importare il file CSS e usare i nomi di classe come className. L’unica attenzione riguarda lo stagger: invece di selettori :nth-child statici, assegna la custom property --tl-index direttamente nell’attributo style durante il rendering della lista (style={{ '--tl-index': index }}). In Vue si usa la stessa tecnica con :style="{ '--tl-index': index }".

Le scroll-driven animations sono già sicure in produzione?

Nel 2026 il supporto è ampio: Chrome 115+, Firefox 110+, Safari 18+ coprono oltre il 90% degli utenti globali. Il blocco @supports not (animation-timeline: scroll()) garantisce che i browser più vecchi ricevano comunque le animazioni tradizionali tramite @keyframes con delay. La regola è sempre di testare con il tuo pubblico target: se hai una percentuale significativa di Safari iOS 15-17, il fallback è essenziale.

Come gestisco una timeline con molti item (50+)?

Con molti item, i selettori :nth-child per assegnare --tl-index diventano impraticabili. La soluzione è usare poche righe di JavaScript per assegnare la custom property programmaticamente: document.querySelectorAll('.timeline__item').forEach((el, i) => el.style.setProperty('--tl-index', i)). Questo non rompe l’approccio CSS-first: JS si limita a passare dati, CSS gestisce tutta la logica visiva. Per timeline molto lunghe considera anche la virtualizzazione dei nodi DOM fuori dalla viewport.

Come rendere la timeline accessibile per screen reader?

La struttura HTML proposta è già un buon punto di partenza. Assicurati di: (1) usare sempre <time datetime="..."> con formato ISO per le date, (2) aggiungere aria-label significativo sulla <section> contenitore, (3) mantenere aria-hidden="true" sui marker puramente decorativi, (4) verificare che il contrasto testo/sfondo rispetti WCAG AA (ratio 4.5:1 minimo per testo normale). Il media query prefers-reduced-motion gestisce gli utenti che hanno disabilitato le animazioni a livello di sistema.

Conclusione

Abbiamo costruito una timeline verticale CSS completa: struttura HTML semantica con <time> e ARIA, stile pulito con custom properties, animazioni di ingresso con stagger via @keyframes, effetti hover interattivi, e scroll-driven animations per la linea di progresso — tutto senza JavaScript.

Il punto chiave è l’architettura a variabili CSS: cambiando pochi valori nel blocco :root o sull’elemento padre, puoi adattare colori, dimensioni e spaziature a qualsiasi design system. La variante alternata desktop mostra come lo stesso codice base si estenda con poche righe aggiuntive.

Il passo successivo naturale è sperimentare con i marker personalizzati: icone SVG inline, numeri progressivi o emoji tematiche al posto dei semplici pallini. Con la struttura che hai ora, è questione di sostituire il contenuto di .timeline__marker e aggiustare qualche misura.

Suggerimenti e Risorse

🔧 Tool: Usa il DevTools di Chrome (pannello “Animations”) per ispezionare e rallentare le scroll-driven animations durante lo sviluppo. Trovi il pannello in DevTools > More Tools > Animations.

💡 Pro tip: Assegna will-change: transform solo agli elementi che animano attivamente — non a tutta la lista. Usarlo su troppi elementi consuma GPU memory e può peggiorare le performance invece di migliorarle.

🎯 Next step: Se vuoi aggiungere animazioni più elaborate (come una linea tratteggiata che si disegna) dai un’occhiata alla tecnica stroke-dashoffset su SVG inline — combinata con scroll-driven animations, crea effetti visivi straordinari con pochissimo codice.

Condividi

Articoli Recenti

Categorie popolari