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.
Diseño Responsivo
Sí
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.
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">
<!-- Controles de navegación -->
<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>
<!-- Indicador de progreso -->
<div class="timeline-progress">
<div class="timeline-progress-bar"></div>
</div>
<!-- Línea de tiempo vertical -->
<div class="timeline-vertical">
<div class="timeline">
<!-- Elemento de línea de tiempo 2024 -->
<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>
<!-- Elemento de línea de tiempo 2023 -->
<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>
<!-- Elemento de línea de tiempo 2022 -->
<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>
<!-- Elemento de línea de tiempo 2021 -->
<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>
<!-- Línea de tiempo horizontal (oculta por defecto) -->
<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 {
/* Colores */
--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;
/* Espaciado */
--timeline-item-spacing: 2rem;
--timeline-marker-size: 1rem;
--timeline-line-width: 2px;
--timeline-content-padding: 1.5rem;
--timeline-border-radius: 0.75rem;
/* Animación */
--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;
/* Tipografía */
--timeline-title-size: 1.25rem;
--timeline-description-size: 0.875rem;
--timeline-date-size: 0.75rem;
/* Sombras */
--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);
}
/* Navegación */
.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);
}
/* Indicador de progreso */
.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%;
}
/* Línea de tiempo vertical */
.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;
}
/* Línea de tiempo horizontal */
.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;
}
/* Animaciones */
@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;
}
/* Diseño responsivo */
@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;
}
}
/* Estados de accesibilidad */
@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;
}
}
/* Modo de alto contraste */
@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;
}
}
/* Tema oscuro */
@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);
}
}
/* Efectos de enfoque para accesibilidad */
.timeline-content:focus,
.timeline-nav-btn:focus,
.layout-btn:focus {
outline: 2px solid var(--timeline-primary-color);
outline-offset: 2px;
}
/* Indicadores de estado */
.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;
}
/* Efectos de carga */
.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();
// Verificación inicial de animaciones
this.verificarAnimaciones();
}
configurarEventListeners() {
// Eventos de scroll
window.addEventListener('scroll', this.throttle(() => {
this.actualizarProgreso();
this.verificarAnimaciones();
}, 16));
// Eventos de redimensionamiento
window.addEventListener('resize', this.debounce(() => {
this.manejarRedimensionamiento();
}, 250));
// Efectos hover de elementos de línea de tiempo
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() {
// Agregar atributos ARIA
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');
}
});
// Navegación por teclado
this.contenedor.addEventListener('keydown', (e) => {
this.manejarNavegacionTeclado(e);
});
}
animarElemento(elemento) {
if (elemento.classList.contains('animate')) return;
elemento.classList.add('animate');
// Disparar evento personalizado
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;
}
// Resaltar el elemento objetivo
this.resaltarElemento(elementoObjetivo);
this.dispararEvento('navegacionClicada', {
ano,
elementoObjetivo
});
}
resaltarElemento(elemento) {
// Remover resaltados previos
this.elementosLineaTiempo.forEach(e => e.classList.remove('highlighted'));
// Agregar resaltado al elemento objetivo
elemento.classList.add('highlighted');
// Remover resaltado después de la animación
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() {
// Recalcular posiciones y animaciones
this.verificarAnimaciones();
this.actualizarProgreso();
}
// Funciones de utilidad
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);
}
// Métodos de API pública
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);
}
// Actualizar elementos de línea de tiempo
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() {
// Remover event listeners
window.removeEventListener('scroll', this.actualizarProgreso);
window.removeEventListener('resize', this.manejarRedimensionamiento);
// Desconectar observador
if (this.observador) {
this.observador.disconnect();
}
// Limpiar referencias
this.elementosLineaTiempo = [];
this.contenedor = null;
this.lineaTiempo = null;
this.dispararEvento('destruido', {});
}
}
// Auto-inicialización
let componenteLineaTiempo;
document.addEventListener('DOMContentLoaded', () => {
componenteLineaTiempo = new ComponenteLineaTiempo();
});
// Exportar para uso de módulos
if (typeof module !== 'undefined' && module.exports) {
module.exports = ComponenteLineaTiempo;
}
// API global
window.ComponenteLineaTiempo = ComponenteLineaTiempo;
Ejemplos de Uso
Línea de Tiempo Básica
// Inicialización básica
const lineaTiempo = new ComponenteLineaTiempo();
// Escuchar eventos
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
// Agregar nuevo elemento
lineaTiempo.agregarElementoLineaTiempo({
ano: '2024',
fecha: '2024-01-15',
titulo: 'Nuevo Hito',
descripcion: 'Descripción del nuevo hito',
icono: '<svg>...</svg>',
posicion: 'final'
});
// Actualizar elemento existente
lineaTiempo.actualizarElementoLineaTiempo('2023', {
titulo: 'Título Actualizado',
descripcion: 'Nueva descripción'
});
// Eliminar elemento
lineaTiempo.eliminarElementoLineaTiempo('2022');
Cambio de Diseño
// Cambiar a diseño horizontal
lineaTiempo.establecerDiseno('horizontal');
// Obtener diseño actual
const disenoActual = lineaTiempo.obtenerDisenoActual();
console.log('Diseño actual:', disenoActual);
Control de Progreso
// Establecer progreso manualmente
lineaTiempo.establecerProgreso(75);
// Navegar a un año específico
lineaTiempo.scrollAElemento('2023');
Referencia de API
Opciones del Constructor
| Opción | Tipo | Por Defecto | Descripción |
|---|---|---|---|
contenedor | string | ’.timeline-container’ | Selector del contenedor principal |
offsetAnimacion | number | 100 | Offset para activar animaciones (px) |
progresoAutomatico | boolean | true | Actualizar progreso automáticamente |
scrollSuave | boolean | true | Usar 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 elementoano(string): Año del elementofecha(string): Fecha en formato ISOtitulo(string): Título del elementodescripcion(string): Descripción del elementoicono(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 actualizarnuevosDatos(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 animadoano: Año del elementofecha: Fecha del elemento
lineatiempo:elementoClicado
Se dispara cuando se hace clic en un elemento.
Detalle:
elemento: Elemento DOM clicadoano: Año del elementofecha: Fecha del elemento
lineatiempo:hoverElemento
Se dispara en hover de elementos.
Detalle:
elemento: Elemento DOMestaHover: Boolean indicando estado hoverano: Año del elemento
lineatiempo:navegacionClicada
Se dispara al usar navegación por años.
Detalle:
ano: Año navegadoelementoObjetivo: Elemento DOM objetivo
lineatiempo:disenocambiado
Se dispara al cambiar el diseño.
Detalle:
diseno: Nuevo diseñodisenoAnterior: 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
Navegación
.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 {
/* Colores principales */
--timeline-primary-color: #3b82f6;
--timeline-secondary-color: #64748b;
--timeline-accent-color: #f59e0b;
--timeline-success-color: #10b981;
--timeline-warning-color: #f59e0b;
--timeline-error-color: #ef4444;
/* Colores de fondo */
--timeline-bg-color: #ffffff;
--timeline-card-bg: #f8fafc;
--timeline-line-color: #e2e8f0;
/* Tipografía */
--timeline-font-family: 'Inter', sans-serif;
--timeline-font-size-base: 1rem;
--timeline-font-size-title: 1.25rem;
--timeline-font-size-date: 0.875rem;
/* Espaciado */
--timeline-spacing-xs: 0.5rem;
--timeline-spacing-sm: 1rem;
--timeline-spacing-md: 1.5rem;
--timeline-spacing-lg: 2rem;
--timeline-spacing-xl: 3rem;
/* Animaciones */
--timeline-transition-duration: 0.3s;
--timeline-animation-duration: 0.6s;
--timeline-easing: cubic-bezier(0.4, 0, 0.2, 1);
}
Animaciones Personalizadas
/* Animación de entrada personalizada */
@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
/* Tema oscuro */
[data-theme="dark"] {
--timeline-bg-color: #1f2937;
--timeline-card-bg: #374151;
--timeline-line-color: #4b5563;
--timeline-secondary-color: #9ca3af;
}
/* Tema de alto contraste */
[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
Navegación por Teclado
- 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
Navegadores Modernos
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
Polyfills para IE 11
<!-- Intersection Observer -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
<!-- Custom Events -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=CustomEvent"></script>
Consideraciones de Rendimiento
Consejos de Optimización
- Throttling de Scroll: Eventos de scroll optimizados
- Intersection Observer: Detección eficiente de viewport
- Debouncing: Eventos de redimensionamiento optimizados
- Lazy Loading: Carga diferida de contenido
Gestión de Conjuntos de Datos Grandes
// Virtualización para muchos elementos
const lineaTiempoVirtual = new ComponenteLineaTiempo({
virtualizacion: true,
elementosVisibles: 10,
alturaElemento: 200
});
Gestión de Memoria
// Limpiar al destruir
lineaTiempo.destruir();
// Remover referencias
lineaTiempo = null;
Ejemplos de Integración
React
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) {
// Actualizar datos
datos.forEach(elemento => {
lineaTiempoRef.current.agregarElementoLineaTiempo(elemento);
});
}
}, [datos]);
return (
<div ref={contenedorRef} className="timeline-container">
{/* Contenido de línea de tiempo */}
</div>
);
}
Vue
<template>
<div ref="contenedorLineaTiempo" class="timeline-container">
<!-- Contenido de línea de tiempo -->
</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">
<!-- Contenido de línea de tiempo -->
</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>