Creare una timeline interattiva con HTML e Web Components

by Anastasia P.
SHARE
Creare una timeline interattiva con HTML e Web Components
© Guida-HTML5.it

Introduzione

I Web Components permettono di creare elementi HTML personalizzati, autonomi e riutilizzabili in pagine diverse. In questo tutorial realizzeremo un componente chiamato <event-timeline>, utile per rappresentare una sequenza di eventi: la storia di un progetto, le fasi di una procedura, gli aggiornamenti di una pratica o le tappe di un percorso formativo.

Il componente utilizzerà tre tecnologie native del browser:

  • Custom Elements, per definire il nuovo tag HTML;
  • Shadow DOM, per isolare struttura e stili interni;
  • Template e slot, per inserire contenuti personalizzati direttamente dall’esterno.

L’esempio mostrerà una timeline verticale in cui ogni evento può essere selezionato. Quando l’utente sceglie un elemento, il componente emette un evento personalizzato chiamato timeline-select. In questo modo il codice esterno può reagire senza conoscere i dettagli interni del componente.

Codice completo

<!-- Utilizzo del Web Component nella pagina -->
<event-timeline>
  <div slot="event" data-date="10 gennaio 2025" data-title="Analisi dei requisiti">
    Raccolta degli obiettivi e definizione delle funzionalità principali.
  </div>

  <div slot="event" data-date="24 gennaio 2025" data-title="Prototipo">
    Creazione del primo prototipo dell’interfaccia.
  </div>

  <div slot="event" data-date="15 febbraio 2025" data-title="Sviluppo">
    Implementazione del componente e integrazione con il progetto.
  </div>

  <div slot="event" data-date="3 marzo 2025" data-title="Pubblicazione">
    Rilascio della versione iniziale online.
  </div>
</event-timeline>

<output id="timeline-status" aria-live="polite"></output>

<script>
  class EventTimeline extends HTMLElement {
    constructor() {
      super();

      // Crea un DOM isolato dal resto della pagina
      const shadow = this.attachShadow({ mode: "open" });

      // Template interno del componente
      shadow.innerHTML = `
        <style>
          :host {
            display: block;
            max-width: 720px;
            margin: 2rem auto;
            font-family: system-ui, sans-serif;
            color: #1f2937;
          }

          .timeline {
            position: relative;
            display: grid;
            gap: 1.25rem;
            padding: 0.5rem 0 0.5rem 2.5rem;
          }

          .timeline::before {
            content: "";
            position: absolute;
            top: 0;
            bottom: 0;
            left: 0.7rem;
            width: 3px;
            background: #cbd5e1;
          }

          .event {
            position: relative;
            border: 1px solid #dbe3ee;
            border-radius: 0.75rem;
            padding: 1rem;
            background: white;
            box-shadow: 0 2px 8px rgb(15 23 42 / 8%);
            cursor: pointer;
          }

          .event::before {
            content: "";
            position: absolute;
            left: -2.25rem;
            top: 1.2rem;
            width: 1rem;
            height: 1rem;
            border: 3px solid white;
            border-radius: 50%;
            background: #2563eb;
            box-shadow: 0 0 0 2px #2563eb;
          }

          .event:focus-visible,
          .event.selected {
            outline: 3px solid #93c5fd;
            outline-offset: 3px;
          }

          .date {
            display: block;
            margin-bottom: 0.35rem;
            color: #2563eb;
            font-size: 0.875rem;
            font-weight: 700;
          }

          h3 {
            margin: 0 0 0.5rem;
            font-size: 1.1rem;
          }

          p {
            margin: 0;
            line-height: 1.5;
          }
        </style>

        <section class="timeline" aria-label="Cronologia degli eventi">
          <slot name="event"></slot>
        </section>
      `;

      this.timeline = shadow.querySelector(".timeline");
      this.slot = shadow.querySelector("slot");
    }

    connectedCallback() {
      this.renderEvents();

      // La struttura assegnata allo slot può cambiare dinamicamente
      this.slot.addEventListener("slotchange", () => {
        this.renderEvents();
      });
    }

    renderEvents() {
      const elements = this.slot.assignedElements();

      elements.forEach((element, index) => {
        element.classList.add("event");
        element.setAttribute("tabindex", "0");
        element.setAttribute("role", "button");
        element.setAttribute("aria-pressed", "false");

        const date = element.dataset.date || "Data non specificata";
        const title = element.dataset.title || "Evento";

        element.innerHTML = `
          <span class="date">${date}</span>
          <h3>${title}</h3>
          <p>${element.textContent.trim()}</p>
        `;

        element.addEventListener("click", () => {
          this.selectEvent(element, index);
        });

        element.addEventListener("keydown", (event) => {
          if (event.key === "Enter" || event.key === " ") {
            event.preventDefault();
            this.selectEvent(element, index);
          }
        });
      });
    }

    selectEvent(element, index) {
      this.slot.assignedElements().forEach((item) => {
        item.classList.remove("selected");
        item.setAttribute("aria-pressed", "false");
      });

      element.classList.add("selected");
      element.setAttribute("aria-pressed", "true");

      this.dispatchEvent(new CustomEvent("timeline-select", {
        detail: {
          index: index,
          date: element.dataset.date,
          title: element.dataset.title
        },
        bubbles: true
      }));
    }
  }

  // Registra il nuovo elemento HTML
  customElements.define("event-timeline", EventTimeline);

  // Gestione dell’evento dall’esterno del componente
  const status = document.querySelector("#timeline-status");

  document.querySelector("event-timeline").addEventListener(
    "timeline-select",
    (event) => {
      status.textContent =
        "Evento selezionato: " + event.detail.title;
    }
  );
</script>

Spiegazione

Definizione del componente

La classe EventTimeline estende HTMLElement, la classe base di tutti gli elementi HTML. La registrazione viene effettuata con:

customElements.define("event-timeline", EventTimeline);

Il nome deve contenere almeno un trattino. Questa regola distingue gli elementi personalizzati dai tag HTML nativi e riduce il rischio di conflitti futuri.

Shadow DOM e template

La chiamata attachShadow({ mode: "open" }) crea una radice Shadow DOM. Gli stili definiti al suo interno non interferiscono con quelli della pagina principale. Questo rende il componente più prevedibile quando viene inserito in progetti diversi.

Il tag <slot> rappresenta un punto di inserimento per il markup fornito dall’utilizzatore. Gli elementi con slot="event" diventano eventi della timeline senza richiedere una struttura JavaScript complessa.

Ciclo di vita

Il metodo connectedCallback() viene eseguito quando l’elemento entra nel documento. Qui richiamiamo renderEvents(), che prepara gli eventi ricevuti dallo slot. L’ascolto di slotchange consente di aggiornare il componente anche quando gli elementi vengono aggiunti o rimossi dinamicamente.

Accessibilità e interazione

Ogni evento riceve un ruolo interattivo, può ottenere il focus tramite tastiera e reagisce ai tasti Invio e Spazio. L’attributo aria-pressed comunica lo stato dell’elemento selezionato alle tecnologie assistive.

L’evento personalizzato contiene le informazioni principali nella proprietà detail. Grazie a bubbles: true, l’evento può propagarsi verso gli antenati, semplificando l’integrazione con il resto dell’applicazione.

Best practice

  • Usa nomi descrittivi e con trattino per i Custom Elements.
  • Non dipendere da librerie esterne quando le API native sono sufficienti.
  • Prevedi sempre un comportamento sensato se mancano attributi come data-date o data-title.
  • Gestisci l’interazione da tastiera e usa attributi ARIA solo quando aggiungono informazioni utili.
  • Evita di usare innerHTML con dati provenienti direttamente dagli utenti senza opportuna sanitizzazione.
  • Documenta gli eventi personalizzati e la struttura degli slot per facilitare il riutilizzo.
  • Considera l’uso di CSS custom properties se vuoi permettere alla pagina esterna di personalizzare colori e spaziature.

Riepilogo

Abbiamo creato una timeline riutilizzabile basata esclusivamente su HTML, CSS e JavaScript nativo. Il componente usa Custom Elements per definire un nuovo tag, Shadow DOM per isolare gli stili, slot per ricevere contenuti esterni ed eventi personalizzati per comunicare con la pagina.

Questo approccio è adatto a interfacce modulari in cui la stessa funzionalità deve essere inserita in più pagine o progetti mantenendo una struttura coerente e facilmente manutenibile.

Approfondisci con risorse ufficiali

SHARE