interactive
intermediate
timeline
animation
interactivo
responsive
navigation
Categoría · Interactivo Nivel de Dificultad · Intermedio Publicado el · 15 de enero de 2024

Componente de Línea de Tiempo Interactiva

Componente de línea de tiempo moderno con animaciones interactivas, múltiples diseños y navegación fluida para mostrar eventos cronológicos.

#timeline #animation #interactivo #responsive #navigation

Diseño Responsivo

Soporte para Modo Oscuro

No

líneas

80

Compatibilidad del Navegador

No

Vista Previa en Vivo

Interactúa con el componente sin salir de la página.

600px

Componente de Línea de Tiempo Interactiva

Un componente de línea de tiempo moderno y completamente funcional con animaciones suaves, múltiples opciones de diseño y navegación interactiva.

Características

  • Múltiples opciones de diseño: Diseños vertical y horizontal
  • Animaciones interactivas: Animaciones fluidas activadas por scroll
  • Soporte de contenido rico: Texto, imágenes, iconos y multimedia
  • Diseño responsivo: Se adapta perfectamente a todos los tamaños de pantalla
  • Estilos personalizables: Variables CSS para fácil personalización
  • Controles de navegación: Navegación rápida por años y períodos
  • Indicador de progreso: Barra de progreso visual del scroll
  • Soporte de accesibilidad: Navegación por teclado y compatibilidad con lectores de pantalla
<div class="timeline-container">
  
  <div class="timeline-navigation">
    <div class="timeline-nav-buttons">
      <button class="timeline-nav-btn active" data-target="2024">2024</button>
      <button class="timeline-nav-btn" data-target="2023">2023</button>
      <button class="timeline-nav-btn" data-target="2022">2022</button>
      <button class="timeline-nav-btn" data-target="2021">2021</button>
    </div>
    <div class="layout-controls">
      <button class="layout-btn active" data-layout="vertical">
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M12 2L12 22M8 6L12 2L16 6M8 18L12 22L16 18"/>
        </svg>
        Vertical
      </button>
      <button class="layout-btn" data-layout="horizontal">
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M2 12L22 12M6 8L2 12L6 16M18 8L22 12L18 16"/>
        </svg>
        Horizontal
      </button>
    </div>
  </div>

  
  <div class="timeline-progress">
    <div class="timeline-progress-bar"></div>
  </div>

  
  <div class="timeline-vertical">
    <div class="timeline">
      
      <div class="timeline-item" data-year="2024" data-date="2024-01-15">
        <div class="timeline-marker">
          <div class="timeline-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M12 2L13.09 8.26L22 9L13.09 9.74L12 16L10.91 9.74L2 9L10.91 8.26L12 2Z"/>
            </svg>
          </div>
        </div>
        <div class="timeline-content">
          <div class="timeline-date">15 de enero, 2024</div>
          <h3 class="timeline-title">Lanzamiento del Producto Principal</h3>
          <p class="timeline-description">
            Lanzamos exitosamente nuestro producto principal después de meses de desarrollo y pruebas. 
            Esta versión incluye todas las características principales y marca un hito importante en nuestro viaje.
          </p>
          <div class="timeline-media">
            <img src="https://images.unsplash.com/photo-1551434678-e076c223a692?w=400&h=200&fit=crop" alt="Lanzamiento del producto" />
          </div>
        </div>
      </div>

      
      <div class="timeline-item" data-year="2023" data-date="2023-08-20">
        <div class="timeline-marker">
          <div class="timeline-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 7V9L21 9ZM15 11V13L21 13V11L15 11ZM21 15V17L15 17V15L21 15ZM11 7H9L9 9H11V7ZM11 11H9V13H11V11ZM11 15H9V17H11V15Z"/>
            </svg>
          </div>
        </div>
        <div class="timeline-content">
          <div class="timeline-date">20 de agosto, 2023</div>
          <h3 class="timeline-title">Fase Beta Completada</h3>
          <p class="timeline-description">
            Completamos con éxito la fase beta con más de 1,000 usuarios probadores. 
            Los comentarios fueron abrumadoramente positivos y nos ayudaron a refinar las características principales.
          </p>
          <div class="timeline-stats">
            <div class="stat">
              <span class="stat-number">1,000+</span>
              <span class="stat-label">Usuarios Beta</span>
            </div>
            <div class="stat">
              <span class="stat-number">95%</span>
              <span class="stat-label">Satisfacción</span>
            </div>
          </div>
        </div>
      </div>

      
      <div class="timeline-item" data-year="2022" data-date="2022-12-10">
        <div class="timeline-marker">
          <div class="timeline-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M9 11H7V9H9V11ZM13 11H11V9H13V11ZM17 11H15V9H17V11ZM19 3H18V1H16V3H8V1H6V3H5C3.89 3 3.01 3.9 3.01 5L3 19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V8H19V19Z"/>
            </svg>
          </div>
        </div>
        <div class="timeline-content">
          <div class="timeline-date">10 de diciembre, 2022</div>
          <h3 class="timeline-title">Desarrollo Iniciado</h3>
          <p class="timeline-description">
            Comenzamos oficialmente el desarrollo después de una extensa investigación de mercado y planificación. 
            El equipo se expandió a 15 desarrolladores y diseñadores.
          </p>
          <div class="timeline-tags">
            <span class="tag">Desarrollo</span>
            <span class="tag">Planificación</span>
            <span class="tag">Equipo</span>
          </div>
        </div>
      </div>

      
      <div class="timeline-item" data-year="2021" data-date="2021-06-01">
        <div class="timeline-marker">
          <div class="timeline-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M12 2L2 7L12 12L22 7L12 2ZM2 17L12 22L22 17M2 12L12 17L22 12"/>
            </svg>
          </div>
        </div>
        <div class="timeline-content">
          <div class="timeline-date">1 de junio, 2021</div>
          <h3 class="timeline-title">Fundación de la Empresa</h3>
          <p class="timeline-description">
            La empresa fue fundada con la visión de crear soluciones innovadoras que resuelvan problemas del mundo real. 
            Comenzamos con un pequeño equipo de 3 personas apasionadas.
          </p>
          <div class="timeline-quote">
            <blockquote>
              "Cada gran viaje comienza con un solo paso. Hoy damos ese paso hacia el futuro."
            </blockquote>
            <cite>- Fundador y CEO</cite>
          </div>
        </div>
      </div>
    </div>
  </div>

  
  <div class="timeline-horizontal" style="display: none;">
    <div class="timeline-horizontal-container">
      <div class="timeline-horizontal-line"></div>
      <div class="timeline-horizontal-items">
        <div class="timeline-horizontal-item" data-year="2021">
          <div class="timeline-horizontal-marker">
            <div class="timeline-horizontal-icon">
              <svg viewBox="0 0 24 24" fill="currentColor">
                <path d="M12 2L2 7L12 12L22 7L12 2Z"/>
              </svg>
            </div>
          </div>
          <div class="timeline-horizontal-content">
            <div class="timeline-horizontal-year">2021</div>
            <h4>Fundación</h4>
          </div>
        </div>
        
        <div class="timeline-horizontal-item" data-year="2022">
          <div class="timeline-horizontal-marker">
            <div class="timeline-horizontal-icon">
              <svg viewBox="0 0 24 24" fill="currentColor">
                <path d="M9 11H7V9H9V11ZM13 11H11V9H13V11Z"/>
              </svg>
            </div>
          </div>
          <div class="timeline-horizontal-content">
            <div class="timeline-horizontal-year">2022</div>
            <h4>Desarrollo</h4>
          </div>
        </div>
        
        <div class="timeline-horizontal-item" data-year="2023">
          <div class="timeline-horizontal-marker">
            <div class="timeline-horizontal-icon">
              <svg viewBox="0 0 24 24" fill="currentColor">
                <path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6Z"/>
              </svg>
            </div>
          </div>
          <div class="timeline-horizontal-content">
            <div class="timeline-horizontal-year">2023</div>
            <h4>Beta</h4>
          </div>
        </div>
        
        <div class="timeline-horizontal-item" data-year="2024">
          <div class="timeline-horizontal-marker">
            <div class="timeline-horizontal-icon">
              <svg viewBox="0 0 24 24" fill="currentColor">
                <path d="M12 2L13.09 8.26L22 9L13.09 9.74L12 16Z"/>
              </svg>
            </div>
          </div>
          <div class="timeline-horizontal-content">
            <div class="timeline-horizontal-year">2024</div>
            <h4>Lanzamiento</h4>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
:root --timeline-primary-color: #3b82f6;
  --timeline-secondary-color: #64748b;
  --timeline-background-color: #ffffff;
  --timeline-border-color: #e2e8f0;
  --timeline-text-color: #1e293b;
  --timeline-muted-color: #64748b;
  --timeline-success-color: #10b981;
  --timeline-warning-color: #f59e0b;
  --timeline-error-color: #ef4444;--timeline-item-spacing: 2rem;
  --timeline-marker-size: 1rem;
  --timeline-line-width: 2px;
  --timeline-content-padding: 1.5rem;
  --timeline-border-radius: 0.75rem;--timeline-animation-duration: 0.6s;
  --timeline-animation-easing: cubic-bezier(0.4, 0, 0.2, 1);
  --timeline-hover-scale: 1.05;
  --timeline-transition-speed: 0.3s;--timeline-title-size: 1.25rem;
  --timeline-description-size: 0.875rem;
  --timeline-date-size: 0.75rem;--timeline-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  --timeline-shadow-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

.timeline-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--timeline-background-color);
  color: var(--timeline-text-color);
}.timeline-navigation {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
  padding: 1rem;
  background: rgba(255, 255, 255, 0.8);
  backdrop-filter: blur(10px);
  border-radius: var(--timeline-border-radius);
  border: 1px solid var(--timeline-border-color);
  box-shadow: var(--timeline-shadow);
}

.timeline-nav-buttons {
  display: flex;
  gap: 0.5rem;
}

.timeline-nav-btn {
  padding: 0.5rem 1rem;
  border: 1px solid var(--timeline-border-color);
  background: transparent;
  color: var(--timeline-text-color);
  border-radius: 0.5rem;
  cursor: pointer;
  transition: all var(--timeline-transition-speed) ease;
  font-weight: 500;
}

.timeline-nav-btn:hover {
  background: var(--timeline-primary-color);
  color: white;
  transform: translateY(-2px);
}

.timeline-nav-btn.active {
  background: var(--timeline-primary-color);
  color: white;
  border-color: var(--timeline-primary-color);
}

.layout-controls {
  display: flex;
  gap: 0.5rem;
}

.layout-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1rem;
  border: 1px solid var(--timeline-border-color);
  background: transparent;
  color: var(--timeline-text-color);
  border-radius: 0.5rem;
  cursor: pointer;
  transition: all var(--timeline-transition-speed) ease;
  font-size: 0.875rem;
}

.layout-btn svg {
  width: 1rem;
  height: 1rem;
}

.layout-btn:hover {
  background: var(--timeline-secondary-color);
  color: white;
}

.layout-btn.active {
  background: var(--timeline-secondary-color);
  color: white;
  border-color: var(--timeline-secondary-color);
}.timeline-progress {
  position: relative;
  height: 4px;
  background: var(--timeline-border-color);
  border-radius: 2px;
  margin-bottom: 2rem;
  overflow: hidden;
}

.timeline-progress-bar {
  height: 100%;
  background: linear-gradient(90deg, var(--timeline-primary-color), var(--timeline-secondary-color));
  border-radius: 2px;
  transition: width var(--timeline-transition-speed) ease;
  width: 0%;
}.timeline-vertical {
  position: relative;
}

.timeline {
  position: relative;
  padding-left: 2rem;
}

.timeline::before {
  content: '';
  position: absolute;
  left: 1rem;
  top: 0;
  bottom: 0;
  width: var(--timeline-line-width);
  background: linear-gradient(to bottom, var(--timeline-primary-color), var(--timeline-secondary-color));
  border-radius: 1px;
}

.timeline-item {
  position: relative;
  margin-bottom: var(--timeline-item-spacing);
  opacity: 0;
  transform: translateY(30px);
  transition: all var(--timeline-animation-duration) var(--timeline-animation-easing);
}

.timeline-item.animate {
  opacity: 1;
  transform: translateY(0);
}

.timeline-item.highlighted {
  transform: scale(1.02);
}

.timeline-item:nth-child(even) .timeline-content {
  margin-left: 2rem;
}

.timeline-item:nth-child(odd) .timeline-content {
  margin-left: 2rem;
}

.timeline-marker {
  position: absolute;
  left: -2rem;
  top: 0.5rem;
  width: calc(var(--timeline-marker-size) * 2);
  height: calc(var(--timeline-marker-size) * 2);
  background: var(--timeline-background-color);
  border: 3px solid var(--timeline-primary-color);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  transition: all var(--timeline-transition-speed) ease;
}

.timeline-marker:hover,
.timeline-marker.hovered {
  transform: scale(1.2);
  border-color: var(--timeline-secondary-color);
  box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}

.timeline-icon {
  width: 1rem;
  height: 1rem;
  color: var(--timeline-primary-color);
  transition: color var(--timeline-transition-speed) ease;
}

.timeline-marker:hover .timeline-icon,
.timeline-marker.hovered .timeline-icon {
  color: var(--timeline-secondary-color);
}

.timeline-content {
  background: var(--timeline-background-color);
  padding: var(--timeline-content-padding);
  border-radius: var(--timeline-border-radius);
  border: 1px solid var(--timeline-border-color);
  box-shadow: var(--timeline-shadow);
  transition: all var(--timeline-transition-speed) ease;
  cursor: pointer;
}

.timeline-content:hover,
.timeline-content.hovered {
  box-shadow: var(--timeline-shadow-hover);
  transform: translateY(-4px);
  border-color: var(--timeline-primary-color);
}

.timeline-date {
  font-size: var(--timeline-date-size);
  color: var(--timeline-muted-color);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.5rem;
}

.timeline-title {
  font-size: var(--timeline-title-size);
  font-weight: 700;
  color: var(--timeline-text-color);
  margin: 0 0 0.75rem 0;
  line-height: 1.4;
}

.timeline-description {
  font-size: var(--timeline-description-size);
  color: var(--timeline-muted-color);
  line-height: 1.6;
  margin: 0 0 1rem 0;
}

.timeline-media {
  margin: 1rem 0;
  border-radius: 0.5rem;
  overflow: hidden;
}

.timeline-media img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform var(--timeline-transition-speed) ease;
}

.timeline-media:hover img {
  transform: scale(1.05);
}

.timeline-stats {
  display: flex;
  gap: 1rem;
  margin: 1rem 0;
}

.stat {
  text-align: center;
  padding: 0.75rem;
  background: rgba(59, 130, 246, 0.1);
  border-radius: 0.5rem;
  flex: 1;
}

.stat-number {
  display: block;
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--timeline-primary-color);
}

.stat-label {
  display: block;
  font-size: 0.75rem;
  color: var(--timeline-muted-color);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.timeline-tags {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin: 1rem 0;
}

.tag {
  padding: 0.25rem 0.75rem;
  background: var(--timeline-primary-color);
  color: white;
  border-radius: 1rem;
  font-size: 0.75rem;
  font-weight: 500;
}

.timeline-quote {
  margin: 1rem 0;
  padding: 1rem;
  background: rgba(100, 116, 139, 0.1);
  border-left: 4px solid var(--timeline-secondary-color);
  border-radius: 0 0.5rem 0.5rem 0;
}

.timeline-quote blockquote {
  margin: 0 0 0.5rem 0;
  font-style: italic;
  color: var(--timeline-text-color);
}

.timeline-quote cite {
  font-size: 0.875rem;
  color: var(--timeline-muted-color);
  font-weight: 500;
}.timeline-horizontal {
  padding: 2rem 0;
}

.timeline-horizontal-container {
  position: relative;
  overflow-x: auto;
  padding: 2rem 0;
}

.timeline-horizontal-line {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: var(--timeline-line-width);
  background: linear-gradient(90deg, var(--timeline-primary-color), var(--timeline-secondary-color));
  transform: translateY(-50%);
}

.timeline-horizontal-items {
  display: flex;
  justify-content: space-between;
  align-items: center;
  min-width: 600px;
  position: relative;
}

.timeline-horizontal-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  opacity: 0;
  transform: translateY(20px);
  transition: all var(--timeline-animation-duration) var(--timeline-animation-easing);
}

.timeline-horizontal-item.animate {
  opacity: 1;
  transform: translateY(0);
}

.timeline-horizontal-marker {
  width: calc(var(--timeline-marker-size) * 2.5);
  height: calc(var(--timeline-marker-size) * 2.5);
  background: var(--timeline-background-color);
  border: 3px solid var(--timeline-primary-color);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1rem;
  transition: all var(--timeline-transition-speed) ease;
  cursor: pointer;
}

.timeline-horizontal-marker:hover {
  transform: scale(1.2);
  border-color: var(--timeline-secondary-color);
  box-shadow: 0 0 0 6px rgba(59, 130, 246, 0.2);
}

.timeline-horizontal-icon {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--timeline-primary-color);
}

.timeline-horizontal-content {
  text-align: center;
  max-width: 120px;
}

.timeline-horizontal-year {
  font-size: 1rem;
  font-weight: 700;
  color: var(--timeline-primary-color);
  margin-bottom: 0.25rem;
}

.timeline-horizontal-content h4 {
  font-size: 0.875rem;
  color: var(--timeline-text-color);
  margin: 0;
  font-weight: 600;
}@keyframes slideInLeft {
  from {
    opacity: 0;
    transform: translateX(-50px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(50px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes pulse {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.05);
  }
}

.timeline-item:nth-child(odd).animate .timeline-content {
  animation: slideInLeft var(--timeline-animation-duration) var(--timeline-animation-easing);
}

.timeline-item:nth-child(even).animate .timeline-content {
  animation: slideInRight var(--timeline-animation-duration) var(--timeline-animation-easing);
}

.timeline-marker.animate {
  animation: pulse 2s infinite;
}@media (max-width: 768px) {
  .timeline-container {
    padding: 1rem;
  }
  
  .timeline-navigation {
    flex-direction: column;
    gap: 1rem;
    align-items: stretch;
  }
  
  .timeline-nav-buttons {
    justify-content: center;
    flex-wrap: wrap;
  }
  
  .layout-controls {
    justify-content: center;
  }
  
  .timeline {
    padding-left: 1.5rem;
  }
  
  .timeline::before {
    left: 0.75rem;
  }
  
  .timeline-marker {
    left: -1.5rem;
  }
  
  .timeline-item:nth-child(even) .timeline-content,
  .timeline-item:nth-child(odd) .timeline-content {
    margin-left: 1.5rem;
  }
  
  .timeline-stats {
    flex-direction: column;
  }
  
  .timeline-horizontal-items {
    min-width: 400px;
  }
  
  .timeline-horizontal-content {
    max-width: 80px;
  }
  
  .timeline-horizontal-content h4 {
    font-size: 0.75rem;
  }
}

@media (max-width: 480px) {
  .timeline-nav-btn {
    padding: 0.375rem 0.75rem;
    font-size: 0.875rem;
  }
  
  .layout-btn {
    padding: 0.375rem 0.75rem;
    font-size: 0.75rem;
  }
  
  .timeline-content {
    padding: 1rem;
  }
  
  .timeline-title {
    font-size: 1.125rem;
  }
  
  .timeline-description {
    font-size: 0.8125rem;
  }
}@media (prefers-reduced-motion: reduce) {
  .timeline-item,
  .timeline-horizontal-item,
  .timeline-marker,
  .timeline-content,
  .timeline-media img {
    animation: none !important;
    transition: none !important;
  }
  
  .timeline-item.animate {
    opacity: 1;
    transform: none;
  }
  
  .timeline-horizontal-item.animate {
    opacity: 1;
    transform: none;
  }
}@media (prefers-contrast: high) {
  :root {
    --timeline-border-color: #000000;
    --timeline-text-color: #000000;
    --timeline-muted-color: #333333;
  }
  
  .timeline-content {
    border-width: 2px;
  }
  
  .timeline-marker {
    border-width: 4px;
  }
}@media (prefers-color-scheme: dark) {
  :root {
    --timeline-background-color: #1e293b;
    --timeline-text-color: #f1f5f9;
    --timeline-border-color: #334155;
    --timeline-muted-color: #94a3b8;
  }
  
  .timeline-navigation {
    background: rgba(30, 41, 59, 0.8);
  }
  
  .stat {
    background: rgba(59, 130, 246, 0.2);
  }
  
  .timeline-quote {
    background: rgba(100, 116, 139, 0.2);
  }
}.timeline-content:focus,
.timeline-nav-btn:focus,
.layout-btn:focus {
  outline: 2px solid var(--timeline-primary-color);
  outline-offset: 2px;
}.timeline-item[data-status="completed"] .timeline-marker {
  border-color: var(--timeline-success-color);
}

.timeline-item[data-status="in-progress"] .timeline-marker {
  border-color: var(--timeline-warning-color);
  animation: pulse 2s infinite;
}

.timeline-item[data-status="pending"] .timeline-marker {
  border-color: var(--timeline-error-color);
  opacity: 0.6;
}.timeline-loading {
  opacity: 0.5;
  pointer-events: none;
}

.timeline-loading .timeline-marker {
  animation: pulse 1s infinite;
}
class ComponenteLineaTiempo {
  constructor(opciones = {}) {
    this.opciones = {
      contenedor: '.timeline-container',
      offsetAnimacion: 100,
      progresoAutomatico: true,
      scrollSuave: true,
      ...opciones
    };
    
    this.contenedor = document.querySelector(this.opciones.contenedor);
    this.lineaTiempo = null;
    this.elementosLineaTiempo = [];
    this.disenoActual = 'vertical';
    this.estaAnimando = false;
    
    this.inicializar();
  }

  inicializar() {
    if (!this.contenedor) {
      console.error('Contenedor de línea de tiempo no encontrado');
      return;
    }
    
    this.lineaTiempo = this.contenedor.querySelector('.timeline');
    this.elementosLineaTiempo = Array.from(this.contenedor.querySelectorAll('.timeline-item'));
    
    this.configurarEventListeners();
    this.configurarObservadorInterseccion();
    this.configurarSeguimientoProgreso();
    this.configurarNavegacion();
    this.configurarCambioDiseno();
    this.configurarAccesibilidad();

    this.verificarAnimaciones();
  }

  configurarEventListeners() {

    window.addEventListener('scroll', this.throttle(() => {
      this.actualizarProgreso();
      this.verificarAnimaciones();
    }, 16));

    window.addEventListener('resize', this.debounce(() => {
      this.manejarRedimensionamiento();
    }, 250));

    this.elementosLineaTiempo.forEach(elemento => {
      elemento.addEventListener('mouseenter', () => {
        this.manejarHoverElemento(elemento, true);
      });
      
      elemento.addEventListener('mouseleave', () => {
        this.manejarHoverElemento(elemento, false);
      });
      
      elemento.addEventListener('click', () => {
        this.manejarClickElemento(elemento);
      });
    });
  }

  configurarObservadorInterseccion() {
    const opcionesObservador = {
      threshold: 0.1,
      rootMargin: `${this.opciones.offsetAnimacion}px 0px`
    };
    
    this.observador = new IntersectionObserver((entradas) => {
      entradas.forEach(entrada => {
        if (entrada.isIntersecting) {
          this.animarElemento(entrada.target);
        }
      });
    }, opcionesObservador);
    
    this.elementosLineaTiempo.forEach(elemento => {
      this.observador.observe(elemento);
    });
  }

  configurarSeguimientoProgreso() {
    this.barraProgreso = this.contenedor.querySelector('.timeline-progress-bar');
    if (this.barraProgreso) {
      this.actualizarProgreso();
    }
  }

  configurarNavegacion() {
    const botonesNav = this.contenedor.querySelectorAll('.timeline-nav-btn');
    botonesNav.forEach(btn => {
      btn.addEventListener('click', () => {
        const objetivo = btn.dataset.target;
        this.navegarAAno(objetivo);
        this.actualizarBotonNavActivo(btn);
      });
    });
  }

  configurarCambioDiseno() {
    const botonesDiseno = this.contenedor.querySelectorAll('.layout-btn');
    botonesDiseno.forEach(btn => {
      btn.addEventListener('click', () => {
        const diseno = btn.dataset.layout;
        this.cambiarDiseno(diseno);
        this.actualizarBotonDisenoActivo(btn);
      });
    });
  }

  configurarAccesibilidad() {

    this.elementosLineaTiempo.forEach((elemento, indice) => {
      elemento.setAttribute('role', 'article');
      elemento.setAttribute('aria-label', `Elemento de línea de tiempo ${indice + 1}`);
      
      const contenido = elemento.querySelector('.timeline-content');
      if (contenido) {
        contenido.setAttribute('tabindex', '0');
      }
    });

    this.contenedor.addEventListener('keydown', (e) => {
      this.manejarNavegacionTeclado(e);
    });
  }

  animarElemento(elemento) {
    if (elemento.classList.contains('animate')) return;
    
    elemento.classList.add('animate');

    this.dispararEvento('elementoAnimado', {
      elemento,
      ano: elemento.dataset.year,
      fecha: elemento.dataset.date
    });
  }

  verificarAnimaciones() {
    if (this.estaAnimando) return;
    
    this.elementosLineaTiempo.forEach(elemento => {
      if (this.estaEnViewport(elemento) && !elemento.classList.contains('animate')) {
        this.animarElemento(elemento);
      }
    });
  }

  estaEnViewport(elemento) {
    const rect = elemento.getBoundingClientRect();
    const alturaVentana = window.innerHeight || document.documentElement.clientHeight;
    
    return (
      rect.top <= alturaVentana - this.opciones.offsetAnimacion &&
      rect.bottom >= this.opciones.offsetAnimacion
    );
  }

  actualizarProgreso() {
    if (!this.barraProgreso || !this.opciones.progresoAutomatico) return;
    
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const alturaDocumento = document.documentElement.scrollHeight - window.innerHeight;
    const progreso = Math.min(scrollTop / alturaDocumento, 1);
    
    this.barraProgreso.style.width = `${progreso * 100}%`;
  }

  navegarAAno(ano) {
    const elementoObjetivo = this.contenedor.querySelector(`[data-year="${ano}"]`);
    if (!elementoObjetivo) return;
    
    this.estaAnimando = true;
    
    if (this.opciones.scrollSuave) {
      elementoObjetivo.scrollIntoView({
        behavior: 'smooth',
        block: 'center'
      });
      
      setTimeout(() => {
        this.estaAnimando = false;
      }, 1000);
    } else {
      elementoObjetivo.scrollIntoView({ block: 'center' });
      this.estaAnimando = false;
    }

    this.resaltarElemento(elementoObjetivo);
    
    this.dispararEvento('navegacionClicada', {
      ano,
      elementoObjetivo
    });
  }

  resaltarElemento(elemento) {

    this.elementosLineaTiempo.forEach(e => e.classList.remove('highlighted'));

    elemento.classList.add('highlighted');

    setTimeout(() => {
      elemento.classList.remove('highlighted');
    }, 2000);
  }

  cambiarDiseno(diseno) {
    if (this.disenoActual === diseno) return;
    
    const lineaTiempoVertical = this.contenedor.querySelector('.timeline-vertical');
    const lineaTiempoHorizontal = this.contenedor.querySelector('.timeline-horizontal');
    
    if (diseno === 'vertical') {
      lineaTiempoVertical.style.display = 'block';
      lineaTiempoHorizontal.style.display = 'none';
    } else if (diseno === 'horizontal') {
      lineaTiempoVertical.style.display = 'none';
      lineaTiempoHorizontal.style.display = 'block';
    }
    
    this.disenoActual = diseno;
    
    this.dispararEvento('disenocambiado', {
      diseno,
      disenoAnterior: this.disenoActual
    });
  }

  actualizarBotonNavActivo(botonActivo) {
    const botonesNav = this.contenedor.querySelectorAll('.timeline-nav-btn');
    botonesNav.forEach(btn => btn.classList.remove('active'));
    botonActivo.classList.add('active');
  }

  actualizarBotonDisenoActivo(botonActivo) {
    const botonesDiseno = this.contenedor.querySelectorAll('.layout-btn');
    botonesDiseno.forEach(btn => btn.classList.remove('active'));
    botonActivo.classList.add('active');
  }

  manejarHoverElemento(elemento, estaHover) {
    const marcador = elemento.querySelector('.timeline-marker');
    const contenido = elemento.querySelector('.timeline-content');
    
    if (estaHover) {
      marcador?.classList.add('hovered');
      contenido?.classList.add('hovered');
    } else {
      marcador?.classList.remove('hovered');
      contenido?.classList.remove('hovered');
    }
    
    this.dispararEvento('hoverElemento', {
      elemento,
      estaHover,
      ano: elemento.dataset.year
    });
  }

  manejarClickElemento(elemento) {
    const ano = elemento.dataset.year;
    const fecha = elemento.dataset.date;
    
    this.dispararEvento('elementoClicado', {
      elemento,
      ano,
      fecha
    });
  }

  manejarNavegacionTeclado(e) {
    const elementoEnfocado = document.activeElement;
    const indiceActual = this.elementosLineaTiempo.indexOf(elementoEnfocado.closest('.timeline-item'));
    
    if (indiceActual === -1) return;
    
    let siguienteIndice;
    
    switch (e.key) {
      case 'ArrowDown':
      case 'ArrowRight':
        e.preventDefault();
        siguienteIndice = Math.min(indiceActual + 1, this.elementosLineaTiempo.length - 1);
        break;
      case 'ArrowUp':
      case 'ArrowLeft':
        e.preventDefault();
        siguienteIndice = Math.max(indiceActual - 1, 0);
        break;
      case 'Home':
        e.preventDefault();
        siguienteIndice = 0;
        break;
      case 'End':
        e.preventDefault();
        siguienteIndice = this.elementosLineaTiempo.length - 1;
        break;
      default:
        return;
    }
    
    const siguienteElemento = this.elementosLineaTiempo[siguienteIndice];
    const siguienteContenido = siguienteElemento.querySelector('.timeline-content');
    if (siguienteContenido) {
      siguienteContenido.focus();
    }
  }

  manejarRedimensionamiento() {

    this.verificarAnimaciones();
    this.actualizarProgreso();
  }

  throttle(func, limite) {
    let enThrottle;
    return function() {
      const args = arguments;
      const contexto = this;
      if (!enThrottle) {
        func.apply(contexto, args);
        enThrottle = true;
        setTimeout(() => enThrottle = false, limite);
      }
    };
  }

  debounce(func, espera) {
    let timeout;
    return function funcionEjecutada(...args) {
      const despues = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(despues, espera);
    };
  }

  dispararEvento(nombreEvento, detalle) {
    const evento = new CustomEvent(`lineatiempo:${nombreEvento}`, {
      detail: detalle,
      bubbles: true,
      cancelable: true
    });
    this.contenedor.dispatchEvent(evento);
  }

  agregarElementoLineaTiempo(datosElemento) {
    const { ano, fecha, titulo, descripcion, icono, posicion } = datosElemento;
    
    const htmlElemento = `
      <div class="timeline-item" data-year="${ano}" data-date="${fecha}">
        <div class="timeline-marker">
          <div class="timeline-icon">
            ${icono || '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>'}
          </div>
        </div>
        <div class="timeline-content">
          <div class="timeline-date">${new Date(fecha).toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' })}</div>
          <h3 class="timeline-title">${titulo}</h3>
          <p class="timeline-description">${descripcion}</p>
        </div>
      </div>
    `;
    
    const lineaTiempo = this.contenedor.querySelector('.timeline-vertical');
    if (posicion === 'inicio') {
      lineaTiempo.insertAdjacentHTML('afterbegin', htmlElemento);
    } else {
      lineaTiempo.insertAdjacentHTML('beforeend', htmlElemento);
    }

    this.elementosLineaTiempo = Array.from(this.contenedor.querySelectorAll('.timeline-item'));
    this.configurarEventListeners();
    
    this.dispararEvento('elementoAgregado', { datosElemento });
  }

  eliminarElementoLineaTiempo(ano) {
    const elemento = this.contenedor.querySelector(`[data-year="${ano}"]`);
    if (elemento) {
      elemento.remove();
      this.elementosLineaTiempo = Array.from(this.contenedor.querySelectorAll('.timeline-item'));
      this.dispararEvento('elementoEliminado', { ano });
    }
  }

  actualizarElementoLineaTiempo(ano, nuevosDatos) {
    const elemento = this.contenedor.querySelector(`[data-year="${ano}"]`);
    if (!elemento) return;
    
    const contenido = elemento.querySelector('.timeline-content');
    if (nuevosDatos.titulo) {
      const elementoTitulo = contenido.querySelector('.timeline-title');
      if (elementoTitulo) elementoTitulo.textContent = nuevosDatos.titulo;
    }
    
    if (nuevosDatos.descripcion) {
      const elementoDesc = contenido.querySelector('.timeline-description');
      if (elementoDesc) elementoDesc.textContent = nuevosDatos.descripcion;
    }
    
    if (nuevosDatos.fecha) {
      elemento.dataset.date = nuevosDatos.fecha;
      const elementoFecha = contenido.querySelector('.timeline-date');
      if (elementoFecha) {
        elementoFecha.textContent = new Date(nuevosDatos.fecha).toLocaleDateString('es-ES', {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        });
      }
    }
    
    this.dispararEvento('elementoActualizado', { ano, nuevosDatos });
  }

  obtenerDatosLineaTiempo() {
    return this.elementosLineaTiempo.map(elemento => ({
      ano: elemento.dataset.year,
      fecha: elemento.dataset.date,
      titulo: elemento.querySelector('.timeline-title')?.textContent,
      descripcion: elemento.querySelector('.timeline-description')?.textContent
    }));
  }

  establecerProgreso(porcentaje) {
    if (this.barraProgreso) {
      this.barraProgreso.style.width = `${Math.min(Math.max(porcentaje, 0), 100)}%`;
    }
  }

  scrollAElemento(ano) {
    this.navegarAAno(ano);
  }

  obtenerDisenoActual() {
    return this.disenoActual;
  }

  establecerDiseno(diseno) {
    this.cambiarDiseno(diseno);
    const botonDiseno = this.contenedor.querySelector(`[data-layout="${diseno}"]`);
    if (botonDiseno) {
      this.actualizarBotonDisenoActivo(botonDiseno);
    }
  }

  destruir() {

    window.removeEventListener('scroll', this.actualizarProgreso);
    window.removeEventListener('resize', this.manejarRedimensionamiento);

    if (this.observador) {
      this.observador.disconnect();
    }

    this.elementosLineaTiempo = [];
    this.contenedor = null;
    this.lineaTiempo = null;
    
    this.dispararEvento('destruido', {});
  }
}

let componenteLineaTiempo;
document.addEventListener('DOMContentLoaded', () => {
  componenteLineaTiempo = new ComponenteLineaTiempo();
});

if (typeof module !== 'undefined' && module.exports) {
  module.exports = ComponenteLineaTiempo;
}

window.ComponenteLineaTiempo = ComponenteLineaTiempo;

Ejemplos de Uso

Línea de Tiempo Básica


const lineaTiempo = new ComponenteLineaTiempo();

document.addEventListener('lineatiempo:elementoAnimado', (e) => {
  console.log('Elemento animado:', e.detail);
});

Configuración Personalizada

const lineaTiempo = new ComponenteLineaTiempo({
  contenedor: '#mi-linea-tiempo',
  offsetAnimacion: 150,
  progresoAutomatico: false,
  scrollSuave: true
});

Gestión Dinámica


lineaTiempo.agregarElementoLineaTiempo({
  ano: '2024',
  fecha: '2024-01-15',
  titulo: 'Nuevo Hito',
  descripcion: 'Descripción del nuevo hito',
  icono: '<svg>...</svg>',
  posicion: 'final'
});

lineaTiempo.actualizarElementoLineaTiempo('2023', {
  titulo: 'Título Actualizado',
  descripcion: 'Nueva descripción'
});

lineaTiempo.eliminarElementoLineaTiempo('2022');

Cambio de Diseño


lineaTiempo.establecerDiseno('horizontal');

const disenoActual = lineaTiempo.obtenerDisenoActual();
console.log('Diseño actual:', disenoActual);

Control de Progreso


lineaTiempo.establecerProgreso(75);

lineaTiempo.scrollAElemento('2023');

Referencia de API

Opciones del Constructor

OpciónTipoPor DefectoDescripción
contenedorstring’.timeline-container’Selector del contenedor principal
offsetAnimacionnumber100Offset para activar animaciones (px)
progresoAutomaticobooleantrueActualizar progreso automáticamente
scrollSuavebooleantrueUsar scroll suave para navegación

Métodos

agregarElementoLineaTiempo(datosElemento)

Agrega un nuevo elemento a la línea de tiempo.

Parámetros:

  • datosElemento (Object): Datos del elemento
    • ano (string): Año del elemento
    • fecha (string): Fecha en formato ISO
    • titulo (string): Título del elemento
    • descripcion (string): Descripción del elemento
    • icono (string): HTML del icono (opcional)
    • posicion (string): ‘inicio’ o ‘final’ (opcional)

eliminarElementoLineaTiempo(ano)

Elimina un elemento de la línea de tiempo.

Parámetros:

  • ano (string): Año del elemento a eliminar

actualizarElementoLineaTiempo(ano, nuevosDatos)

Actualiza un elemento existente.

Parámetros:

  • ano (string): Año del elemento a actualizar
  • nuevosDatos (Object): Nuevos datos del elemento

obtenerDatosLineaTiempo()

Retorna todos los datos de la línea de tiempo.

Retorna: Array de objetos con datos de elementos

establecerProgreso(porcentaje)

Establece el progreso manualmente.

Parámetros:

  • porcentaje (number): Porcentaje de progreso (0-100)

scrollAElemento(ano)

Navega a un elemento específico.

Parámetros:

  • ano (string): Año del elemento objetivo

obtenerDisenoActual()

Retorna el diseño actual.

Retorna: string (‘vertical’ o ‘horizontal’)

establecerDiseno(diseno)

Cambia el diseño de la línea de tiempo.

Parámetros:

  • diseno (string): ‘vertical’ o ‘horizontal’

destruir()

Destruye la instancia y limpia los event listeners.

Eventos

lineatiempo:elementoAnimado

Se dispara cuando un elemento se anima.

Detalle:

  • elemento: Elemento DOM animado
  • ano: Año del elemento
  • fecha: Fecha del elemento

lineatiempo:elementoClicado

Se dispara cuando se hace clic en un elemento.

Detalle:

  • elemento: Elemento DOM clicado
  • ano: Año del elemento
  • fecha: Fecha del elemento

lineatiempo:hoverElemento

Se dispara en hover de elementos.

Detalle:

  • elemento: Elemento DOM
  • estaHover: Boolean indicando estado hover
  • ano: Año del elemento

lineatiempo:navegacionClicada

Se dispara al usar navegación por años.

Detalle:

  • ano: Año navegado
  • elementoObjetivo: Elemento DOM objetivo

lineatiempo:disenocambiado

Se dispara al cambiar el diseño.

Detalle:

  • diseno: Nuevo diseño
  • disenoAnterior: Diseño anterior

Clases CSS

Diseños

  • .timeline-vertical: Diseño vertical
  • .timeline-horizontal: Diseño horizontal
  • .timeline-compact: Diseño compacto

Estados

  • .animate: Elemento animado
  • .highlighted: Elemento resaltado
  • .hovered: Elemento en hover
  • .active: Elemento activo
  • .timeline-nav-btn: Botón de navegación
  • .timeline-nav-btn.active: Botón activo
  • .layout-btn: Botón de cambio de diseño
  • .layout-btn.active: Botón de diseño activo

Personalización

Variables CSS

:root --timeline-primary-color: #3b82f6;
  --timeline-secondary-color: #64748b;
  --timeline-accent-color: #f59e0b;
  --timeline-success-color: #10b981;
  --timeline-warning-color: #f59e0b;
  --timeline-error-color: #ef4444;--timeline-bg-color: #ffffff;
  --timeline-card-bg: #f8fafc;
  --timeline-line-color: #e2e8f0;--timeline-font-family: 'Inter', sans-serif;
  --timeline-font-size-base: 1rem;
  --timeline-font-size-title: 1.25rem;
  --timeline-font-size-date: 0.875rem;--timeline-spacing-xs: 0.5rem;
  --timeline-spacing-sm: 1rem;
  --timeline-spacing-md: 1.5rem;
  --timeline-spacing-lg: 2rem;
  --timeline-spacing-xl: 3rem;--timeline-transition-duration: 0.3s;
  --timeline-animation-duration: 0.6s;
  --timeline-easing: cubic-bezier(0.4, 0, 0.2, 1);
}

Animaciones Personalizadas

@keyframes entradaPersonalizada {
  0% {
    opacity: 0;
    transform: translateX(-50px) scale(0.8);
  }
  50% {
    opacity: 0.5;
    transform: translateX(-10px) scale(0.95);
  }
  100% {
    opacity: 1;
    transform: translateX(0) scale(1);
  }
}

.timeline-item.animate {
  animation: entradaPersonalizada var(--timeline-animation-duration) var(--timeline-easing);
}

Integración con Temas

[data-theme="dark"] {
  --timeline-bg-color: #1f2937;
  --timeline-card-bg: #374151;
  --timeline-line-color: #4b5563;
  --timeline-secondary-color: #9ca3af;
}[data-theme="high-contrast"] {
  --timeline-primary-color: #000000;
  --timeline-bg-color: #ffffff;
  --timeline-line-color: #000000;
}

Accesibilidad

Soporte ARIA

  • Roles semánticos apropiados
  • Etiquetas descriptivas
  • Estados y propiedades ARIA
  • Anuncios para lectores de pantalla
  • Flecha Arriba/Abajo: Navegar entre elementos
  • Flecha Izquierda/Derecha: Navegar entre elementos (horizontal)
  • Inicio: Ir al primer elemento
  • Fin: Ir al último elemento
  • Tab: Navegar por elementos enfocables

Soporte para Lectores de Pantalla

  • Contenido descriptivo
  • Anuncios de cambios de estado
  • Estructura semántica clara

Reducción de Movimiento

@media (prefers-reduced-motion: reduce) {
  .timeline-item {
    animation: none;
    transition: none;
  }
  
  .timeline-progress-bar {
    transition: none;
  }
}

Soporte de Navegadores

  • Chrome 60+
  • Firefox 55+
  • Safari 12+
  • Edge 79+

Polyfills para IE 11


<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>


<script src="https://polyfill.io/v3/polyfill.min.js?features=CustomEvent"></script>

Consideraciones de Rendimiento

Consejos de Optimización

  1. Throttling de Scroll: Eventos de scroll optimizados
  2. Intersection Observer: Detección eficiente de viewport
  3. Debouncing: Eventos de redimensionamiento optimizados
  4. Lazy Loading: Carga diferida de contenido

Gestión de Conjuntos de Datos Grandes


const lineaTiempoVirtual = new ComponenteLineaTiempo({
  virtualizacion: true,
  elementosVisibles: 10,
  alturaElemento: 200
});

Gestión de Memoria


lineaTiempo.destruir();

lineaTiempo = null;

Ejemplos de Integración

React

x
import React, { useEffect, useRef } from 'react';
import { ComponenteLineaTiempo } from './ComponenteLineaTiempo';

function LineaTiempoReact({ datos, opciones }) {
  const contenedorRef = useRef(null);
  const lineaTiempoRef = useRef(null);
  
  useEffect(() => {
    if (contenedorRef.current) {
      lineaTiempoRef.current = new ComponenteLineaTiempo({
        contenedor: contenedorRef.current,
        ...opciones
      });
    }
    
    return () => {
      if (lineaTiempoRef.current) {
        lineaTiempoRef.current.destruir();
      }
    };
  }, [opciones]);
  
  useEffect(() => {
    if (lineaTiempoRef.current && datos) {

      datos.forEach(elemento => {
        lineaTiempoRef.current.agregarElementoLineaTiempo(elemento);
      });
    }
  }, [datos]);
  
  return (
    <div ref={contenedorRef} className="timeline-container">
      
    </div>
  );
}

Vue

<template>
  <div ref="contenedorLineaTiempo" class="timeline-container">
    
  </div>
</template>

<script>
import { ComponenteLineaTiempo } from './ComponenteLineaTiempo';

export default {
  name: 'LineaTiempoVue',
  props: {
    datos: Array,
    opciones: Object
  },
  data() {
    return {
      lineaTiempo: null
    };
  },
  mounted() {
    this.lineaTiempo = new ComponenteLineaTiempo({
      contenedor: this.$refs.contenedorLineaTiempo,
      ...this.opciones
    });
  },
  beforeDestroy() {
    if (this.lineaTiempo) {
      this.lineaTiempo.destruir();
    }
  },
  watch: {
    datos: {
      handler(nuevosDatos) {
        if (this.lineaTiempo && nuevosDatos) {
          nuevosDatos.forEach(elemento => {
            this.lineaTiempo.agregarElementoLineaTiempo(elemento);
          });
        }
      },
      deep: true
    }
  }
};
</script>

Angular

import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ComponenteLineaTiempo } from './ComponenteLineaTiempo';

@Component({
  selector: 'app-linea-tiempo',
  template: `
    <div #contenedorLineaTiempo class="timeline-container">
      
    </div>
  `
})
export class LineaTiempoComponent implements OnInit, OnDestroy {
  @ViewChild('contenedorLineaTiempo', { static: true }) contenedorRef!: ElementRef;
  @Input() datos: any[] = [];
  @Input() opciones: any = {};
  
  private lineaTiempo: ComponenteLineaTiempo | null = null;
  
  ngOnInit() {
    this.lineaTiempo = new ComponenteLineaTiempo({
      contenedor: this.contenedorRef.nativeElement,
      ...this.opciones
    });
    
    if (this.datos) {
      this.datos.forEach(elemento => {
        this.lineaTiempo!.agregarElementoLineaTiempo(elemento);
      });
    }
  }
  
  ngOnDestroy() {
    if (this.lineaTiempo) {
      this.lineaTiempo.destruir();
    }
  }
}

HTML

20

líneas

CSS

60

líneas


                <div class="timeline-container">
  <div class="timeline">
    <div class="timeline-item">
      <div class="timeline-marker"></div>
      <div class="timeline-content">
        <h3>Evento 1</h3>
        <p>Descripción del primer evento</p>
        <span class="timeline-date">2024</span>
      </div>
    </div>
    <div class="timeline-item">
      <div class="timeline-marker"></div>
      <div class="timeline-content">
        <h3>Evento 2</h3>
        <p>Descripción del segundo evento</p>
        <span class="timeline-date">2023</span>
      </div>
    </div>
  </div>
</div>

              
20líneas
583caracteres
HTMLIdioma

Fragmentos de Código Relacionados

Explora packs de plantillas

¿Necesitas bloques más grandes? Descubre landings y colecciones de componentes.

Abrir la biblioteca de plantillas ->