Línea del tiempo en HTML y CSS: 3 variantes con código (2026)

Una línea del tiempo con HTML y CSS es uno de esos componentes que mejoran cualquier currículum web, página de historia de marca, roadmap de producto o cronología de eventos. En esta guía vas a construir tres variantes desde cero (vertical clásica, vertical alternada en zig-zag, y horizontal con scroll), con código copy-paste listo para tu proyecto, demos en vivo renderizadas dentro del propio post para que veas el resultado antes de pegarlo, responsive sin frameworks, animaciones de aparición al hacer scroll y consejos de accesibilidad.

Cero JavaScript en las dos variantes verticales. Solo HTML semántico y CSS moderno (Grid, custom properties, clamp). La horizontal añade un toque mínimo de JavaScript opcional para scroll-snap suavizado, pero también funciona sin él. Vamos al grano.

Demo final: cómo se ve lo que vamos a construir

Antes de mirar una sola línea de código, esto es lo que vamos a tener al terminar la primera variante (vertical clásica). Funciona en cualquier navegador moderno y se adapta al ancho del contenedor:

  1. Investigación y wireframes

    Fase de descubrimiento con stakeholders y prototipo low-fi en Figma.

  2. Diseño visual y sistema de diseño

    Identidad, tipografía, colores y librería de componentes.

  3. Desarrollo MVP

    Frontend con React + backend en Node. Despliegue en staging.

  4. Lanzamiento y mejora continua

    Beta pública, recogida de feedback y iteración bisemanal.

Esa demo está renderizada con el mismo código que vas a copiar más abajo. Cero imágenes, cero dependencias, cero JavaScript.

El HTML semántico (con la etiqueta time)

Una línea del tiempo, semánticamente, es una lista ordenada de eventos. Cada evento tiene una fecha (etiqueta <time> con atributo datetime en formato ISO 8601), un título y una descripción. El HTML base es el siguiente:

<ol class="tl">
  <li>
    <time datetime="2024-01">Enero 2024</time>
    <h3>Investigación y wireframes</h3>
    <p>Descubrimiento con stakeholders y prototipo low-fi en Figma.</p>
  </li>
  <li>
    <time datetime="2024-04">Abril 2024</time>
    <h3>Diseño visual</h3>
    <p>Identidad, tipografía y librería de componentes.</p>
  </li>
  <!-- ...más eventos -->
</ol>

Tres detalles importantes del HTML que marcan diferencia:

  • Usa <ol> en vez de <ul> o <div>: los eventos tienen orden cronológico, y los lectores de pantalla anuncian «1 de 4», «2 de 4″… lo que ayuda a la accesibilidad.
  • La etiqueta <time datetime="..."> es clave: Google la entiende como dato estructurado y los lectores de pantalla la leen como fecha. Usa siempre el formato ISO (YYYY-MM-DD o YYYY-MM).
  • Jerarquía de encabezados correcta: si el timeline está dentro de una sección H2 del documento, los títulos de eventos van con H3. No saltes niveles.

El CSS base con variables y reset

Antes de los estilos visuales del timeline, definimos variables CSS para que sea fácil customizar colores y tamaños desde un solo sitio. Esto te permite cambiar el color de la línea, los puntos o la tipografía cambiando una variable, sin tocar el resto del código.

.tl {
  --tl-color: #2E5AAC;       /* color de la línea y los puntos */
  --tl-bg: #ffffff;          /* fondo de los puntos */
  --tl-text: #1f2937;        /* color del texto principal */
  --tl-muted: #6b7280;       /* color de la fecha y descripciones */
  --tl-border: 3px;          /* grosor de la línea */
  --tl-dot: 15px;            /* tamaño del punto */
  --tl-gap: clamp(1rem, 2vw, 1.5rem);

  list-style: none;
  margin: 0;
  padding: 0;
  font-family: system-ui, -apple-system, sans-serif;
}

El uso de clamp() en --tl-gap hace que el espaciado entre eventos se adapte al ancho de la pantalla sin necesidad de media queries: mínimo 1rem en móvil, máximo 1.5rem en desktop, y proporcional al viewport en medio.

Variante 1: línea del tiempo vertical clásica

Es la opción más común y la que mejor funciona en mobile sin necesidad de breakpoints. La línea vertical va a la izquierda, los puntos sobre ella y el contenido a la derecha. Este es el CSS completo:

.tl {
  padding-left: 2rem;
  border-left: var(--tl-border) solid var(--tl-color);
}

.tl li {
  position: relative;
  padding-bottom: var(--tl-gap);
}

.tl li:last-child {
  padding-bottom: 0;
}

/* el punto sobre la línea */
.tl li::before {
  content: "";
  position: absolute;
  left: calc(-2rem - (var(--tl-dot) / 2) - (var(--tl-border) / 2));
  top: 0.3rem;
  width: var(--tl-dot);
  height: var(--tl-dot);
  background: var(--tl-bg);
  border: var(--tl-border) solid var(--tl-color);
  border-radius: 50%;
}

.tl time {
  display: block;
  font-size: 0.85rem;
  color: var(--tl-muted);
  font-weight: 600;
  margin-bottom: 0.2rem;
}

.tl h3 {
  margin: 0 0 0.3rem;
  color: var(--tl-text);
  font-size: 1.05rem;
}

.tl p {
  margin: 0;
  color: var(--tl-muted);
  line-height: 1.5;
}

La clave está en el pseudoelemento ::before del <li>: con position: absolute lo colocamos exactamente sobre la línea (que es el border-left del <ol>). El cálculo de left está pensado para que el punto quede perfectamente centrado encima del borde, sin importar el grosor que elijas en --tl-border o --tl-dot.

Variante 2: línea del tiempo vertical alternada (zig-zag)

Es la variante «elegante» que ves en webs de marca, currículums y páginas de historia. La línea va por el centro y los eventos alternan izquierda y derecha. Demo en vivo:

  1. Fundación de la marca

    Primer prototipo y validación con 30 clientes piloto.

  2. Equipo de 10

    Crecimiento orgánico y expansión a tres países europeos.

  3. Ronda de serie A

    Inversión de 5M para internacionalización y producto B2B.

  4. Salida a mercados US

    Apertura de oficina en Nueva York y plantilla de 80 personas.

Para construirla, el truco está en alternar márgenes con :nth-child(odd) y :nth-child(even), y crear la línea central con un ::before sobre el <ol>:

.tl-zigzag {
  list-style: none;
  padding: 0;
  margin: 0 auto;
  max-width: 700px;
  position: relative;
}

.tl-zigzag::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--tl-color);
  transform: translateX(-50%);
}

.tl-zigzag li {
  position: relative;
  width: 45%;
  margin-bottom: 1.5rem;
  padding: 1rem;
  background: #fff;
  border-radius: 6px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.tl-zigzag li:nth-child(odd) { margin-right: 55%; }
.tl-zigzag li:nth-child(even) { margin-left: 55%; }

.tl-zigzag li::after {
  content: "";
  position: absolute;
  top: 1rem;
  width: 13px;
  height: 13px;
  background: #fff;
  border: 3px solid var(--tl-color);
  border-radius: 50%;
}

.tl-zigzag li:nth-child(odd)::after { right: -28px; }
.tl-zigzag li:nth-child(even)::after { left: -28px; }

/* responsive: colapsa a una columna en móvil */
@media (max-width: 600px) {
  .tl-zigzag::before { left: 20px; }
  .tl-zigzag li {
    width: auto;
    margin-left: 50px !important;
    margin-right: 0 !important;
  }
  .tl-zigzag li::after { left: -38px !important; right: auto !important; }
}

El bloque @media (max-width: 600px) es importante: una timeline alternada en móvil no funciona porque las tarjetas quedarían demasiado estrechas. Por eso colapsamos a la variante clásica (vertical de un solo lado) por debajo de 600px.

Variante 3: línea del tiempo horizontal con scroll-snap

Las timelines horizontales se usan mucho en landings de roadmap, páginas de historia de empresa y galerías de proyectos. Con CSS moderno (Flexbox + scroll-snap) se construye sin JavaScript:

  1. Auditoría inicial

    Análisis técnico y de UX del producto actual.

  2. Rediseño UI

    Nueva identidad y librería de componentes.

  3. Migración técnica

    Stack moderno y refactor del backend.

  4. Lanzamiento beta

    Acceso anticipado para 200 usuarios.

  5. Salida pública

    Marketing internacional y mercado abierto.

  6. Iteración continua

    Ciclos quincenales de mejora basados en feedback.

Pasa el dedo o desliza horizontalmente (arrastrando o usando la rueda del ratón con Shift en desktop) para ver cómo cada tarjeta se «engancha» a la siguiente gracias a scroll-snap-align. El código:

.tl-horizontal {
  list-style: none;
  padding: 0 1.5rem;
  margin: 0;
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
}

.tl-horizontal li {
  flex: 0 0 240px;
  scroll-snap-align: start;
  padding: 1rem;
  background: #fff;
  border-radius: 6px;
  border-top: 3px solid var(--tl-color);
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.tl-horizontal time {
  display: block;
  color: var(--tl-color);
  font-weight: 700;
  font-size: 0.85rem;
  margin-bottom: 0.4rem;
}

Tres cosas que mejoran la experiencia:

  • flex: 0 0 240px fija el ancho de cada tarjeta y evita que crezcan o se compriman según el número de elementos.
  • scroll-snap-type: x mandatory en el contenedor + scroll-snap-align: start en los hijos crea el «enganche» al hacer scroll.
  • Padding en el contenedor en lugar de margin en los hijos: mantiene la primera tarjeta alineada a la izquierda y la última a la derecha al hacer scroll.

Tip: Si quieres timelines aún más avanzadas (con conectores, animaciones complejas, integración con CMS), echa un ojo a Envato Elements: tienes plantillas HTML/CSS premium con timelines listos para insertar en cualquier proyecto.

Animaciones de aparición al hacer scroll

Una vez funciona la timeline básica, una mejora visual sencilla es que cada evento aparezca con un fade-in al hacer scroll. Lo hacemos con IntersectionObserver (API nativa, soportada en todos los navegadores modernos):

/* CSS: estado inicial y final */
.tl li {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.tl li.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Respeta a usuarios que prefieren menos movimiento */
@media (prefers-reduced-motion: reduce) {
  .tl li {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
// JavaScript: añade la clase "visible" cuando el elemento entra en viewport
const items = document.querySelectorAll('.tl li');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.2 });

items.forEach((item) => observer.observe(item));

Tres detalles importantes: el threshold: 0.2 dispara la animación cuando el 20% del elemento es visible (más natural que el 0% por defecto), el observer.unobserve() deja de mirar el elemento una vez animado (rendimiento), y el bloque prefers-reduced-motion del CSS desactiva la animación para usuarios con esa preferencia activa en el sistema operativo (accesibilidad).

Accesibilidad: ARIA, contraste y prefers-reduced-motion

Una línea del tiempo accesible es la diferencia entre un componente que funciona para todo el mundo y uno que solo funciona para personas sin discapacidad. Tres puntos críticos:

  • Etiqueta el contenedor con aria-label: añade aria-label="Cronología de la empresa" al <ol> para que lectores de pantalla anuncien qué es esta lista. Usa una etiqueta descriptiva del contenido.
  • Contraste mínimo 4.5:1 entre el texto y el fondo (WCAG AA). El gris #6b7280 sobre fondo blanco da 4.83:1 — pasa. Si bajas más, no pasa. Comprueba con WebAIM Contrast Checker.
  • Respeta prefers-reduced-motion: ya lo vimos en la sección de animaciones. Es obligatorio si añades cualquier animación de scroll, transición o movimiento.

Una cosa que NO debes hacer: usar <div> con role="list" en lugar de <ol>. La etiqueta semántica nativa siempre gana en accesibilidad. El role es solo para casos donde no puedas usar el elemento nativo.

Ejemplos en CodePen para inspirarte

Si quieres ver implementaciones más complejas (con animaciones SVG, conectores curvos, scroll-driven animations, etc.), estos cinco CodePen son los que más me gustan:

Preguntas frecuentes sobre líneas del tiempo en HTML y CSS

¿Cuál es la mejor etiqueta HTML para una línea del tiempo?

Una lista ordenada (<ol>) porque los eventos tienen orden cronológico. Cada evento es un <li>. Dentro del <li>, la fecha debe ir envuelta en una etiqueta <time datetime="YYYY-MM-DD"> para que sea legible por máquinas (Google la entiende como dato estructurado y los lectores de pantalla la anuncian como fecha).

¿Hace falta JavaScript para una línea del tiempo en CSS?

No para las variantes verticales (clásica y alternada): se construyen 100% con HTML + CSS usando pseudoelementos ::before/::after y position: absolute. La variante horizontal con scroll-snap también funciona sin JavaScript. Solo necesitas JS si quieres animaciones de aparición al hacer scroll (con IntersectionObserver) o navegación interactiva con botones.

¿Cómo hago que la línea del tiempo sea responsive?

La variante vertical clásica ya es responsive por defecto: ocupa el ancho del contenedor y el contenido se adapta. Para la variante alternada (zig-zag), añade un media query @media (max-width: 600px) que la colapse a una sola columna (línea a la izquierda, todas las tarjetas al mismo lado). La horizontal con scroll-snap es naturalmente responsive porque scrollea cuando no cabe.

¿Cómo cambio el color de la línea y los puntos?

Cambia el valor de la variable CSS --tl-color en el contenedor. Si quieres un degradado en lugar de un color sólido, sustituye border-left: 3px solid var(--tl-color) por un background: linear-gradient(...) sobre un ::before del <ol> con el ancho de la línea.

¿Funciona en todos los navegadores?

Sí. Custom properties CSS (variables), clamp(), scroll-snap-type e IntersectionObserver están soportados en todos los navegadores modernos (Chrome, Firefox, Safari, Edge) desde 2020+. Si necesitas soporte de Internet Explorer 11 (raro hoy), tendrás que reemplazar variables CSS y clamp() por valores fijos.

¿Puedo usar una línea del tiempo en WordPress sin instalar un plugin?

Sí. Pega el HTML de cualquiera de las tres variantes dentro de un bloque «HTML personalizado» en el editor de Gutenberg, y el CSS en Apariencia → Personalizar → CSS adicional. Si lo vas a usar en varias páginas, mejor pasar el CSS al style.css del tema hijo para que se cachee correctamente.

¿Hay plugins de WordPress para líneas del tiempo?

Existen (Cool Timeline, Timeline Block, Post Timeline), pero los desaconsejo si lo único que necesitas es una timeline estática como las de este tutorial. Los plugins añaden peso, dependencias y JavaScript innecesario. Para una timeline simple, copiar y pegar el HTML+CSS de este post da mejor rendimiento y total control sobre el diseño.

¿Cómo añado iconos en cada evento de la línea del tiempo?

Dos opciones: (1) sustituir el ::before circular por un ::before con background-image: url(icono.svg) y dimensiones del SVG, o (2) añadir un <span class="tl-icon"> dentro de cada <li> con el icono como contenido (SVG inline o icon font tipo Lucide/Heroicons). La opción 2 es más limpia si los iconos varían entre eventos.

Conclusión

Tienes tres caminos según tu proyecto. Para currículums, blogs y secciones «Sobre nosotros»: la variante vertical clásica es la opción rápida y siempre funciona. Para landings de marca o páginas de historia con tarjetas detalladas: la alternada en zig-zag aporta dinamismo y aprovecha mejor el ancho. Para roadmaps de producto, galerías de proyectos o cronologías largas: la horizontal con scroll-snap es la mejor experiencia en mobile y desktop.

El código de las tres lo tienes copy-paste arriba. Si te ha resultado útil, comparte el post o déjame en comentarios qué variante has terminado usando. Disfrútalo!


Más recursos HTML5 y CSS3

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio