Categoría · Animaciones de Carga Nivel de Dificultad · Intermedio Publicado el · 15 de enero de 2024

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

#upload #progress #animations #spinners #indicators #ui

Diseño Responsivo

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.

600px

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étodoDescripciónParámetros
establecerProgreso(id, progreso)Establece el progreso de un elementoid: string, progreso: number (0-100)
mostrarCarga(id)Muestra estado de carga en un botónid: string
ocultarCarga(id, estado)Oculta estado de cargaid: string, estado: ‘default’|‘success’|‘error’
crearSpinner(tipo, tamaño)Crea un spinner dinámicamentetipo: 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>

              
4líneas
162caracteres
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 →