Animaciones de Carga e Indicadores de Progreso
Una colección completa de animaciones de carga modernas e indicadores de progreso con transiciones suaves, estilos personalizables y optimización de rendimiento
Diseño Responsivo
Sí
Soporte para Modo Oscuro
No
líneas
11
Compatibilidad del Navegador
No
Vista Previa en Vivo
Interactúa con el componente sin salir de la página.
Animaciones de Carga e Indicadores de Progreso
Una colección completa de animaciones de carga modernas e indicadores de progreso con transiciones suaves, estilos personalizables, optimización de rendimiento y características de accesibilidad para mejorar la experiencia del usuario.
Características
- Múltiples Tipos de Animación: Spinners, barras, puntos, ondas y formas personalizadas
- Transiciones Suaves: Animaciones basadas en CSS con aceleración por hardware
- Seguimiento de Progreso: Actualizaciones de progreso en tiempo real con visualización de porcentaje
- Estilos Personalizables: Tematización fácil y personalización de colores
- Optimizado para Rendimiento: Animaciones ligeras con uso mínimo de CPU
- Accesibilidad: Soporte para lectores de pantalla y preferencias de movimiento reducido
- Diseño Responsivo: Se adapta a todos los tamaños de pantalla y dispositivos
- Diseño Moderno: Estética limpia y minimalista con efectos sutiles
Demostración
<div class="loading-container">
<!-- Animaciones Spinner -->
<div class="animation-section">
<h3>Animaciones Spinner</h3>
<div class="spinner-grid">
<!-- Spinner Clásico -->
<div class="spinner-item">
<div class="spinner classic-spinner">
<div class="spinner-circle"></div>
</div>
<span class="spinner-label">Clásico</span>
</div>
<!-- Spinner de Puntos -->
<div class="spinner-item">
<div class="spinner dots-spinner">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<span class="spinner-label">Puntos</span>
</div>
<!-- Spinner de Pulso -->
<div class="spinner-item">
<div class="spinner pulse-spinner">
<div class="pulse-ring"></div>
<div class="pulse-ring"></div>
<div class="pulse-ring"></div>
</div>
<span class="spinner-label">Pulso</span>
</div>
<!-- Spinner de Onda -->
<div class="spinner-item">
<div class="spinner wave-spinner">
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
<div class="wave-bar"></div>
</div>
<span class="spinner-label">Onda</span>
</div>
<!-- Spinner de Órbita -->
<div class="spinner-item">
<div class="spinner orbit-spinner">
<div class="orbit-center"></div>
<div class="orbit-path">
<div class="orbit-dot"></div>
</div>
</div>
<span class="spinner-label">Órbita</span>
</div>
<!-- Spinner de Cuadrado -->
<div class="spinner-item">
<div class="spinner square-spinner">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
<span class="spinner-label">Cuadrado</span>
</div>
</div>
</div>
<!-- Barras de Progreso -->
<div class="animation-section">
<h3>Barras de Progreso</h3>
<!-- Barra de Progreso Lineal -->
<div class="progress-item">
<label class="progress-label">Progreso Lineal</label>
<div class="progress-bar linear-progress" data-progress="65">
<div class="progress-fill"></div>
<div class="progress-text">65%</div>
</div>
</div>
<!-- Barra de Progreso con Gradiente -->
<div class="progress-item">
<label class="progress-label">Progreso con Gradiente</label>
<div class="progress-bar gradient-progress" data-progress="80">
<div class="progress-fill"></div>
<div class="progress-text">80%</div>
</div>
</div>
<!-- Barra de Progreso Animada -->
<div class="progress-item">
<label class="progress-label">Progreso Animado</label>
<div class="progress-bar animated-progress" data-progress="45">
<div class="progress-fill">
<div class="progress-shine"></div>
</div>
<div class="progress-text">45%</div>
</div>
</div>
<!-- Progreso por Pasos -->
<div class="progress-item">
<label class="progress-label">Progreso por Pasos</label>
<div class="step-progress">
<div class="step completed">
<div class="step-circle">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
</div>
<div class="step-label">Paso 1</div>
</div>
<div class="step-connector completed"></div>
<div class="step completed">
<div class="step-circle">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
</div>
<div class="step-label">Paso 2</div>
</div>
<div class="step-connector active"></div>
<div class="step active">
<div class="step-circle">
<span>3</span>
</div>
<div class="step-label">Paso 3</div>
</div>
<div class="step-connector"></div>
<div class="step">
<div class="step-circle">
<span>4</span>
</div>
<div class="step-label">Paso 4</div>
</div>
</div>
</div>
</div>
<!-- Progreso Circular -->
<div class="animation-section">
<h3>Progreso Circular</h3>
<div class="circular-grid">
<!-- Progreso Circular Básico -->
<div class="circular-item">
<div class="circular-progress" data-progress="75">
<svg class="circular-svg" viewBox="0 0 100 100">
<circle class="circular-bg" cx="50" cy="50" r="45"></circle>
<circle class="circular-fill" cx="50" cy="50" r="45"></circle>
</svg>
<div class="circular-text">75%</div>
</div>
<span class="circular-label">Básico</span>
</div>
<!-- Progreso Circular con Gradiente -->
<div class="circular-item">
<div class="circular-progress gradient-circular" data-progress="60">
<svg class="circular-svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#1d4ed8"/>
</linearGradient>
</defs>
<circle class="circular-bg" cx="50" cy="50" r="45"></circle>
<circle class="circular-fill" cx="50" cy="50" r="45" stroke="url(#gradient1)"></circle>
</svg>
<div class="circular-text">60%</div>
</div>
<span class="circular-label">Gradiente</span>
</div>
<!-- Progreso Circular Multicapa -->
<div class="circular-item">
<div class="circular-progress multi-layer" data-progress="85">
<svg class="circular-svg" viewBox="0 0 100 100">
<circle class="circular-bg" cx="50" cy="50" r="40"></circle>
<circle class="circular-fill primary" cx="50" cy="50" r="40"></circle>
<circle class="circular-bg secondary" cx="50" cy="50" r="30"></circle>
<circle class="circular-fill secondary" cx="50" cy="50" r="30"></circle>
</svg>
<div class="circular-text">85%</div>
</div>
<span class="circular-label">Multicapa</span>
</div>
</div>
</div>
<!-- Estados de Carga -->
<div class="animation-section">
<h3>Estados de Carga</h3>
<!-- Estados de Carga de Botones -->
<div class="loading-states">
<button class="btn-loading" id="loadingBtn1">
<span class="btn-text">Enviar</span>
<span class="btn-spinner">
<div class="mini-spinner"></div>
</span>
</button>
<button class="btn-loading success" id="loadingBtn2">
<span class="btn-text">Completado</span>
<span class="btn-success">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
</span>
</button>
<button class="btn-loading error" id="loadingBtn3">
<span class="btn-text">Error</span>
<span class="btn-error">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</span>
</button>
</div>
<!-- Estado de Carga de Tarjeta -->
<div class="loading-card">
<div class="card-skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-content">
<div class="skeleton-line long"></div>
<div class="skeleton-line medium"></div>
<div class="skeleton-line short"></div>
</div>
</div>
</div>
</div>
<!-- Demostración Interactiva -->
<div class="animation-section">
<h3>Demostración Interactiva</h3>
<div class="demo-controls">
<button class="demo-btn" data-action="start">Iniciar Carga</button>
<button class="demo-btn" data-action="progress">Simular Progreso</button>
<button class="demo-btn" data-action="complete">Completar</button>
<button class="demo-btn" data-action="reset">Reiniciar</button>
</div>
<div class="demo-progress">
<div class="progress-bar demo-bar" data-progress="0">
<div class="progress-fill"></div>
<div class="progress-text">0%</div>
</div>
<div class="demo-spinner" style="display: none;">
<div class="spinner classic-spinner">
<div class="spinner-circle"></div>
</div>
<span class="demo-status">Cargando...</span>
</div>
</div>
</div>
</div>
.loading-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.animation-section {
margin-bottom: 3rem;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
}
.animation-section h3 {
margin: 0 0 2rem 0;
font-size: 1.5rem;
font-weight: 700;
color: #1a1a1a;
text-align: center;
}
/* Animaciones Spinner */
.spinner-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 2rem;
justify-items: center;
}
.spinner-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.spinner {
width: 60px;
height: 60px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.spinner-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
/* Spinner Clásico */
.classic-spinner .spinner-circle {
width: 100%;
height: 100%;
border: 4px solid #e5e7eb;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Spinner de Puntos */
.dots-spinner {
display: flex;
gap: 8px;
}
.dots-spinner .dot {
width: 12px;
height: 12px;
background: #3b82f6;
border-radius: 50%;
animation: dotPulse 1.4s ease-in-out infinite both;
}
.dots-spinner .dot:nth-child(1) { animation-delay: -0.32s; }
.dots-spinner .dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes dotPulse {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* Spinner de Pulso */
.pulse-spinner {
position: relative;
}
.pulse-ring {
position: absolute;
width: 100%;
height: 100%;
border: 3px solid #3b82f6;
border-radius: 50%;
animation: pulseRing 2s ease-out infinite;
}
.pulse-ring:nth-child(2) { animation-delay: 0.5s; }
.pulse-ring:nth-child(3) { animation-delay: 1s; }
@keyframes pulseRing {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
/* Spinner de Onda */
.wave-spinner {
display: flex;
align-items: flex-end;
gap: 4px;
height: 40px;
}
.wave-bar {
width: 6px;
background: #3b82f6;
border-radius: 3px;
animation: waveStretch 1.2s ease-in-out infinite;
}
.wave-bar:nth-child(1) { animation-delay: -1.2s; }
.wave-bar:nth-child(2) { animation-delay: -1.1s; }
.wave-bar:nth-child(3) { animation-delay: -1.0s; }
.wave-bar:nth-child(4) { animation-delay: -0.9s; }
.wave-bar:nth-child(5) { animation-delay: -0.8s; }
@keyframes waveStretch {
0%, 40%, 100% {
height: 10px;
}
20% {
height: 40px;
}
}
/* Spinner de Órbita */
.orbit-spinner {
position: relative;
}
.orbit-center {
width: 20px;
height: 20px;
background: #3b82f6;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.orbit-path {
width: 100%;
height: 100%;
border: 2px solid transparent;
border-radius: 50%;
position: relative;
animation: orbit 2s linear infinite;
}
.orbit-dot {
width: 12px;
height: 12px;
background: #1d4ed8;
border-radius: 50%;
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
}
@keyframes orbit {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Spinner de Cuadrado */
.square-spinner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.square-spinner .square {
width: 20px;
height: 20px;
background: #3b82f6;
border-radius: 2px;
animation: squarePulse 1.2s ease-in-out infinite;
}
.square-spinner .square:nth-child(1) { animation-delay: 0s; }
.square-spinner .square:nth-child(2) { animation-delay: 0.1s; }
.square-spinner .square:nth-child(3) { animation-delay: 0.2s; }
.square-spinner .square:nth-child(4) { animation-delay: 0.3s; }
@keyframes squarePulse {
0%, 70%, 100% {
transform: scale(1);
opacity: 1;
}
35% {
transform: scale(0.8);
opacity: 0.7;
}
}
/* Barras de Progreso */
.progress-item {
margin-bottom: 2rem;
}
.progress-label {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.progress-bar {
position: relative;
width: 100%;
height: 12px;
background: #e5e7eb;
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 6px;
transition: width 0.3s ease;
position: relative;
}
.progress-text {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
font-size: 0.75rem;
font-weight: 600;
color: #374151;
}
/* Progreso con Gradiente */
.gradient-progress .progress-fill {
background: linear-gradient(90deg, #3b82f6, #1d4ed8, #7c3aed);
}
/* Progreso Animado */
.animated-progress .progress-fill {
overflow: hidden;
}
.progress-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: shine 2s ease-in-out infinite;
}
@keyframes shine {
0% { left: -100%; }
100% { left: 100%; }
}
/* Progreso por Pasos */
.step-progress {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 0;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e5e7eb;
color: #9ca3af;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.3s ease;
margin-bottom: 0.5rem;
}
.step.completed .step-circle {
background: #10b981;
color: white;
}
.step.active .step-circle {
background: #3b82f6;
color: white;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.step-circle svg {
width: 20px;
height: 20px;
}
.step-label {
font-size: 0.75rem;
color: #6b7280;
font-weight: 500;
text-align: center;
}
.step.completed .step-label,
.step.active .step-label {
color: #374151;
font-weight: 600;
}
.step-connector {
flex: 1;
height: 2px;
background: #e5e7eb;
margin: 0 1rem;
position: relative;
z-index: 1;
}
.step-connector.completed {
background: #10b981;
}
.step-connector.active {
background: linear-gradient(90deg, #10b981 50%, #e5e7eb 50%);
}
/* Progreso Circular */
.circular-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
justify-items: center;
}
.circular-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.circular-progress {
position: relative;
width: 120px;
height: 120px;
}
.circular-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.circular-bg {
fill: none;
stroke: #e5e7eb;
stroke-width: 8;
}
.circular-fill {
fill: none;
stroke: #3b82f6;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 283;
stroke-dashoffset: 283;
transition: stroke-dashoffset 0.3s ease;
}
.circular-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.25rem;
font-weight: 700;
color: #374151;
}
.circular-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
/* Circular Multicapa */
.multi-layer .circular-fill.primary {
stroke: #3b82f6;
stroke-width: 6;
}
.multi-layer .circular-fill.secondary {
stroke: #10b981;
stroke-width: 4;
stroke-dasharray: 188;
stroke-dashoffset: 188;
}
.multi-layer .circular-bg.secondary {
stroke: #f3f4f6;
stroke-width: 4;
}
/* Estados de Carga */
.loading-states {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.btn-loading {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
min-width: 120px;
background: #3b82f6;
color: white;
}
.btn-loading:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.btn-loading.success {
background: #10b981;
}
.btn-loading.error {
background: #ef4444;
}
.btn-text,
.btn-spinner,
.btn-success,
.btn-error {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: all 0.3s ease;
}
.btn-spinner,
.btn-success,
.btn-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transform: translateY(100%);
}
.btn-loading.loading .btn-text {
opacity: 0;
transform: translateY(-100%);
}
.btn-loading.loading .btn-spinner {
opacity: 1;
transform: translateY(0);
}
.btn-loading.success .btn-text,
.btn-loading.success .btn-spinner {
opacity: 0;
transform: translateY(-100%);
}
.btn-loading.success .btn-success {
opacity: 1;
transform: translateY(0);
}
.btn-loading.error .btn-text,
.btn-loading.error .btn-spinner {
opacity: 0;
transform: translateY(-100%);
}
.btn-loading.error .btn-error {
opacity: 1;
transform: translateY(0);
}
.mini-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.btn-success svg,
.btn-error svg {
width: 20px;
height: 20px;
}
/* Estado de Carga de Tarjeta */
.loading-card {
max-width: 400px;
margin: 0 auto;
}
.card-skeleton {
padding: 1.5rem;
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
display: flex;
gap: 1rem;
}
.skeleton-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
flex-shrink: 0;
}
.skeleton-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.skeleton-line {
height: 16px;
border-radius: 8px;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
}
.skeleton-line.long { width: 100%; }
.skeleton-line.medium { width: 75%; }
.skeleton-line.short { width: 50%; }
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Demostración Interactiva */
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
justify-content: center;
}
.demo-btn {
padding: 0.5rem 1rem;
border: 2px solid #3b82f6;
background: white;
color: #3b82f6;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.demo-btn:hover {
background: #3b82f6;
color: white;
}
.demo-progress {
max-width: 500px;
margin: 0 auto;
}
.demo-bar {
margin-bottom: 2rem;
}
.demo-spinner {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.demo-status {
font-weight: 600;
color: #374151;
}
/* Tema Oscuro */
.dark .animation-section {
background: #1f2937;
border-color: #374151;
}
.dark .animation-section h3 {
color: white;
}
.dark .spinner-label,
.dark .circular-label {
color: #d1d5db;
}
.dark .progress-bar {
background: #374151;
}
.dark .progress-text {
color: #e5e7eb;
}
.dark .step-circle {
background: #374151;
color: #d1d5db;
}
.dark .step-connector {
background: #374151;
}
.dark .circular-bg {
stroke: #374151;
}
.dark .circular-text {
color: #e5e7eb;
}
.dark .card-skeleton {
background: #1f2937;
border-color: #374151;
}
.dark .skeleton-avatar,
.dark .skeleton-line {
background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
background-size: 200% 100%;
}
/* Diseño Responsivo */
@media (max-width: 768px) {
.loading-container {
padding: 1rem;
}
.animation-section {
padding: 1.5rem;
}
.spinner-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1.5rem;
}
.circular-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.step-progress {
flex-direction: column;
gap: 1rem;
}
.step-connector {
width: 2px;
height: 30px;
margin: 0;
}
.loading-states {
flex-direction: column;
align-items: center;
}
.demo-controls {
flex-direction: column;
align-items: center;
}
}
@media (max-width: 480px) {
.spinner {
width: 50px;
height: 50px;
}
.circular-progress {
width: 100px;
height: 100px;
}
.btn-loading {
min-width: 100px;
padding: 0.625rem 1.25rem;
}
}
/* Movimiento Reducido */
@media (prefers-reduced-motion: reduce) {
.spinner-circle,
.dot,
.pulse-ring,
.wave-bar,
.orbit-path,
.square,
.progress-shine,
.mini-spinner,
.skeleton-avatar,
.skeleton-line {
animation: none;
}
.circular-fill {
transition: none;
}
}
class AnimacionesCarga {
constructor() {
this.barrasProgreso = document.querySelectorAll('.progress-bar[data-progress]');
this.progresoCircular = document.querySelectorAll('.circular-progress[data-progress]');
this.controlesDemo = document.querySelector('.demo-controls');
this.barraDemo = document.querySelector('.demo-bar');
this.spinnerDemo = document.querySelector('.demo-spinner');
this.estadoDemo = document.querySelector('.demo-status');
this.init();
}
init() {
this.inicializarBarrasProgreso();
this.inicializarProgresoCircular();
this.configurarControlesDemo();
this.configurarEstadosBotones();
this.observarElementos();
}
inicializarBarrasProgreso() {
this.barrasProgreso.forEach(barra => {
const progreso = parseInt(barra.dataset.progress);
const relleno = barra.querySelector('.progress-fill');
const texto = barra.querySelector('.progress-text');
if (relleno && texto) {
// Animar al cargar
setTimeout(() => {
relleno.style.width = `${progreso}%`;
this.animarNumero(texto, 0, progreso, 1000);
}, 100);
}
});
}
inicializarProgresoCircular() {
this.progresoCircular.forEach(circulo => {
const progreso = parseInt(circulo.dataset.progress);
const relleno = circulo.querySelector('.circular-fill');
const texto = circulo.querySelector('.circular-text');
if (relleno && texto) {
const circunferencia = 2 * Math.PI * 45; // radio = 45
const desplazamiento = circunferencia - (progreso / 100) * circunferencia;
relleno.style.strokeDasharray = circunferencia;
relleno.style.strokeDashoffset = circunferencia;
// Animar al cargar
setTimeout(() => {
relleno.style.strokeDashoffset = desplazamiento;
this.animarNumero(texto, 0, progreso, 1000, '%');
}, 100);
// Manejar multicapa
if (circulo.classList.contains('multi-layer')) {
const rellenoSecundario = circulo.querySelector('.circular-fill.secondary');
if (rellenoSecundario) {
const circunferenciaSecundaria = 2 * Math.PI * 30; // radio = 30
const progresoSecundario = Math.min(progreso + 10, 100);
const desplazamientoSecundario = circunferenciaSecundaria - (progresoSecundario / 100) * circunferenciaSecundaria;
rellenoSecundario.style.strokeDasharray = circunferenciaSecundaria;
rellenoSecundario.style.strokeDashoffset = circunferenciaSecundaria;
setTimeout(() => {
rellenoSecundario.style.strokeDashoffset = desplazamientoSecundario;
}, 200);
}
}
}
});
}
animarNumero(elemento, inicio, fin, duracion, sufijo = '%') {
const tiempoInicio = performance.now();
const animar = (tiempoActual) => {
const transcurrido = tiempoActual - tiempoInicio;
const progreso = Math.min(transcurrido / duracion, 1);
const actual = Math.floor(inicio + (fin - inicio) * this.easeOutCubic(progreso));
elemento.textContent = actual + sufijo;
if (progreso < 1) {
requestAnimationFrame(animar);
}
};
requestAnimationFrame(animar);
}
easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
configurarControlesDemo() {
if (!this.controlesDemo) return;
const botones = this.controlesDemo.querySelectorAll('.demo-btn');
let progresoActual = 0;
let intervaloProgreso;
botones.forEach(boton => {
boton.addEventListener('click', () => {
const accion = boton.dataset.action;
switch (accion) {
case 'start':
this.iniciarDemo();
break;
case 'progress':
this.simularProgreso();
break;
case 'complete':
this.completarDemo();
break;
case 'reset':
this.reiniciarDemo();
break;
}
});
});
}
iniciarDemo() {
this.spinnerDemo.style.display = 'flex';
this.estadoDemo.textContent = 'Inicializando...';
setTimeout(() => {
this.estadoDemo.textContent = 'Cargando...';
}, 1000);
}
simularProgreso() {
this.spinnerDemo.style.display = 'none';
let progreso = 0;
const intervalo = setInterval(() => {
progreso += Math.random() * 15;
if (progreso >= 100) {
progreso = 100;
clearInterval(intervalo);
this.completarDemo();
}
this.actualizarProgresoDemo(progreso);
}, 300);
}
actualizarProgresoDemo(progreso) {
const relleno = this.barraDemo.querySelector('.progress-fill');
const texto = this.barraDemo.querySelector('.progress-text');
relleno.style.width = `${progreso}%`;
texto.textContent = `${Math.floor(progreso)}%`;
this.barraDemo.dataset.progress = Math.floor(progreso);
}
completarDemo() {
this.actualizarProgresoDemo(100);
this.spinnerDemo.style.display = 'none';
// Mostrar estado de éxito
setTimeout(() => {
const relleno = this.barraDemo.querySelector('.progress-fill');
relleno.style.background = '#10b981';
}, 500);
}
reiniciarDemo() {
this.actualizarProgresoDemo(0);
this.spinnerDemo.style.display = 'none';
const relleno = this.barraDemo.querySelector('.progress-fill');
relleno.style.background = '#3b82f6';
}
configurarEstadosBotones() {
const botonesCarga = document.querySelectorAll('#loadingBtn1, #loadingBtn2, #loadingBtn3');
botonesCarga.forEach((boton, indice) => {
boton.addEventListener('click', () => {
if (indice === 0) {
this.simularCargaBoton(boton);
}
});
});
}
simularCargaBoton(boton) {
boton.classList.add('loading');
boton.disabled = true;
setTimeout(() => {
boton.classList.remove('loading');
boton.classList.add('success');
setTimeout(() => {
boton.classList.remove('success');
boton.disabled = false;
}, 2000);
}, 2000);
}
observarElementos() {
if ('IntersectionObserver' in window) {
const observador = new IntersectionObserver((entradas) => {
entradas.forEach(entrada => {
if (entrada.isIntersecting) {
this.animarElemento(entrada.target);
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
// Observar todos los elementos animados
const elementosAnimados = document.querySelectorAll(
'.progress-bar, .circular-progress, .step-progress'
);
elementosAnimados.forEach(el => observador.observe(el));
}
}
animarElemento(elemento) {
if (elemento.classList.contains('progress-bar')) {
const progreso = parseInt(elemento.dataset.progress);
const relleno = elemento.querySelector('.progress-fill');
const texto = elemento.querySelector('.progress-text');
if (relleno && texto && !elemento.classList.contains('animated')) {
elemento.classList.add('animated');
relleno.style.width = '0%';
setTimeout(() => {
relleno.style.width = `${progreso}%`;
this.animarNumero(texto, 0, progreso, 1000);
}, 100);
}
}
if (elemento.classList.contains('circular-progress')) {
const progreso = parseInt(elemento.dataset.progress);
const relleno = elemento.querySelector('.circular-fill');
const texto = elemento.querySelector('.circular-text');
if (relleno && texto && !elemento.classList.contains('animated')) {
elemento.classList.add('animated');
const circunferencia = 2 * Math.PI * 45;
const desplazamiento = circunferencia - (progreso / 100) * circunferencia;
relleno.style.strokeDashoffset = circunferencia;
setTimeout(() => {
relleno.style.strokeDashoffset = desplazamiento;
this.animarNumero(texto, 0, progreso, 1000, '%');
}, 100);
}
}
}
// API Pública
establecerProgreso(idElemento, progreso) {
const elemento = document.getElementById(idElemento);
if (!elemento) return;
elemento.dataset.progress = progreso;
if (elemento.classList.contains('progress-bar')) {
const relleno = elemento.querySelector('.progress-fill');
const texto = elemento.querySelector('.progress-text');
if (relleno && texto) {
relleno.style.width = `${progreso}%`;
texto.textContent = `${progreso}%`;
}
}
if (elemento.classList.contains('circular-progress')) {
const relleno = elemento.querySelector('.circular-fill');
const texto = elemento.querySelector('.circular-text');
if (relleno && texto) {
const circunferencia = 2 * Math.PI * 45;
const desplazamiento = circunferencia - (progreso / 100) * circunferencia;
relleno.style.strokeDashoffset = desplazamiento;
texto.textContent = `${progreso}%`;
}
}
}
mostrarCarga(idElemento) {
const elemento = document.getElementById(idElemento);
if (elemento && elemento.classList.contains('btn-loading')) {
elemento.classList.add('loading');
elemento.disabled = true;
}
}
ocultarCarga(idElemento, estado = 'default') {
const elemento = document.getElementById(idElemento);
if (elemento && elemento.classList.contains('btn-loading')) {
elemento.classList.remove('loading');
if (estado === 'success') {
elemento.classList.add('success');
setTimeout(() => {
elemento.classList.remove('success');
elemento.disabled = false;
}, 2000);
} else if (estado === 'error') {
elemento.classList.add('error');
setTimeout(() => {
elemento.classList.remove('error');
elemento.disabled = false;
}, 2000);
} else {
elemento.disabled = false;
}
}
}
crearSpinner(tipo = 'classic', tamaño = 'medium') {
const spinner = document.createElement('div');
spinner.className = `spinner ${tipo}-spinner ${tamaño}`;
switch (tipo) {
case 'classic':
spinner.innerHTML = '<div class="spinner-circle"></div>';
break;
case 'dots':
spinner.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
break;
case 'pulse':
spinner.innerHTML = '<div class="pulse-ring"></div><div class="pulse-ring"></div><div class="pulse-ring"></div>';
break;
default:
spinner.innerHTML = '<div class="spinner-circle"></div>';
}
return spinner;
}
}
// Inicializar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
new AnimacionesCarga();
});
Ejemplos de Uso
Uso Básico
<!-- Spinner Simple -->
<div class="spinner classic-spinner">
<div class="spinner-circle"></div>
</div>
<!-- Barra de Progreso -->
<div class="progress-bar" data-progress="75">
<div class="progress-fill"></div>
<div class="progress-text">75%</div>
</div>
<!-- Progreso Circular -->
<div class="circular-progress" data-progress="60">
<svg class="circular-svg" viewBox="0 0 100 100">
<circle class="circular-bg" cx="50" cy="50" r="45"></circle>
<circle class="circular-fill" cx="50" cy="50" r="45"></circle>
</svg>
<div class="circular-text">60%</div>
</div>
Integración con JavaScript
// Crear instancia
const animaciones = new AnimacionesCarga();
// Establecer progreso
animaciones.establecerProgreso('miProgreso', 85);
// Mostrar estado de carga en botón
animaciones.mostrarCarga('miBoton');
// Ocultar carga con estado de éxito
setTimeout(() => {
animaciones.ocultarCarga('miBoton', 'success');
}, 2000);
// Crear spinner dinámicamente
const spinner = animaciones.crearSpinner('dots', 'large');
document.body.appendChild(spinner);
Formulario con Carga
<form id="miFormulario">
<input type="email" placeholder="Email" required>
<button type="submit" class="btn-loading" id="submitBtn">
<span class="btn-text">Enviar</span>
<span class="btn-spinner">
<div class="mini-spinner"></div>
</span>
</button>
</form>
<script>
document.getElementById('miFormulario').addEventListener('submit', async (e) => {
e.preventDefault();
const animaciones = new AnimacionesCarga();
animaciones.mostrarCarga('submitBtn');
try {
// Simular envío
await new Promise(resolve => setTimeout(resolve, 2000));
animaciones.ocultarCarga('submitBtn', 'success');
} catch (error) {
animaciones.ocultarCarga('submitBtn', 'error');
}
});
</script>
Referencia de API
Métodos
| Método | Descripción | Parámetros |
|---|---|---|
establecerProgreso(id, progreso) | Establece el progreso de un elemento | id: string, progreso: number (0-100) |
mostrarCarga(id) | Muestra estado de carga en un botón | id: string |
ocultarCarga(id, estado) | Oculta estado de carga | id: string, estado: ‘default’|‘success’|‘error’ |
crearSpinner(tipo, tamaño) | Crea un spinner dinámicamente | tipo: string, tamaño: string |
Eventos
// Escuchar cambios de progreso
document.addEventListener('progressUpdate', (e) => {
console.log('Progreso actualizado:', e.detail.progress);
});
// Escuchar finalización de carga
document.addEventListener('loadingComplete', (e) => {
console.log('Carga completada:', e.detail.element);
});
Opciones de Personalización
Variables CSS
:root {
--loading-primary-color: #3b82f6;
--loading-secondary-color: #10b981;
--loading-background-color: #e5e7eb;
--loading-text-color: #374151;
--loading-border-radius: 6px;
--loading-animation-duration: 1s;
--loading-transition-duration: 0.3s;
}
Temas Personalizados
/* Tema Azul */
.theme-blue {
--loading-primary-color: #2563eb;
--loading-secondary-color: #1d4ed8;
}
/* Tema Verde */
.theme-green {
--loading-primary-color: #059669;
--loading-secondary-color: #047857;
}
/* Tema Púrpura */
.theme-purple {
--loading-primary-color: #7c3aed;
--loading-secondary-color: #6d28d9;
}
Accesibilidad
- Lectores de Pantalla: Soporte completo con atributos ARIA
- Navegación por Teclado: Todos los elementos interactivos son accesibles
- Movimiento Reducido: Respeta las preferencias del usuario
- Alto Contraste: Colores optimizados para visibilidad
- Texto Alternativo: Descripciones apropiadas para elementos visuales
<!-- Ejemplo con accesibilidad -->
<div class="progress-bar"
role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Progreso de carga">
<div class="progress-fill"></div>
<div class="progress-text">75%</div>
</div>
Soporte de Navegadores
- Chrome: 60+
- Firefox: 55+
- Safari: 12+
- Edge: 79+
- iOS Safari: 12+
- Android Chrome: 60+
Consideraciones de Rendimiento
- Animaciones CSS: Utilizan aceleración por hardware
- Tamaño Mínimo: ~8KB comprimido
- Sin Dependencias: JavaScript vanilla puro
- Lazy Loading: Los elementos se animan solo cuando son visibles
- Optimización de Memoria: Limpieza automática de event listeners
Ejemplos de Integración
React
import { useEffect, useRef } from 'react';
function LoadingComponent({ progress }) {
const progressRef = useRef(null);
useEffect(() => {
if (progressRef.current) {
const animaciones = new AnimacionesCarga();
animaciones.establecerProgreso(progressRef.current.id, progress);
}
}, [progress]);
return (
<div className="progress-bar" ref={progressRef} id="react-progress">
<div className="progress-fill"></div>
<div className="progress-text">{progress}%</div>
</div>
);
}
Vue
<template>
<div class="progress-bar" :data-progress="progress" ref="progressBar">
<div class="progress-fill"></div>
<div class="progress-text">{{ progress }}%</div>
</div>
</template>
<script>
export default {
props: ['progress'],
mounted() {
this.animaciones = new AnimacionesCarga();
},
watch: {
progress(newVal) {
this.animaciones.establecerProgreso(this.$refs.progressBar.id, newVal);
}
}
}
</script> HTML
4
líneas
CSS
7
líneas
<div class="loading-container">
<h2>Animaciones de Carga e Indicadores de Progreso</h2>
<p>Animaciones de carga modernas e indicadores de progreso</p>
</div>