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

Componentes de Formularios Interactivos

Una colección completa de componentes de formularios interactivos modernos con validación en tiempo real, animaciones suaves y experiencia de usuario mejorada

#forms #validation #animations #input #components #ui

Diseño Responsivo

Soporte para Modo Oscuro

No

líneas

134

Compatibilidad del Navegador

No

Vista Previa en Vivo

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

600px

Componentes de Formularios Interactivos

Una colección completa de componentes de formularios modernos e interactivos con validación en tiempo real, animaciones suaves, características de accesibilidad y experiencia de usuario mejorada para aplicaciones web.

Características

  • Múltiples Tipos de Input: Texto, email, contraseña, select, textarea, carga de archivos y más
  • Validación en Tiempo Real: Retroalimentación instantánea con reglas de validación personalizadas
  • Animaciones Suaves: Transiciones y micro-interacciones con CSS
  • Accesibilidad: Navegación completa por teclado y soporte para lectores de pantalla
  • Diseño Responsivo: Se adapta a todos los tamaños de pantalla y dispositivos
  • Estilo Personalizable: Opciones fáciles de tematización y personalización
  • Manejo de Errores: Mensajes de error claros e indicadores visuales
  • Indicadores de Progreso: Seguimiento de progreso para formularios de múltiples pasos

Demo

<div class="form-container">
  <!-- Formulario de Contacto -->
  <form class="interactive-form contact-form" id="contactForm">
    <div class="form-header">
      <h2 class="form-title">Contáctanos</h2>
      <p class="form-description">Nos encantaría saber de ti. Envíanos un mensaje y responderemos lo antes posible.</p>
    </div>

    <div class="form-grid">
      <!-- Campo Nombre -->
      <div class="form-group">
        <label for="name" class="form-label">Nombre Completo</label>
        <div class="input-wrapper">
          <input type="text" id="name" name="name" class="form-input" required>
          <span class="input-focus-border"></span>
          <div class="input-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
            </svg>
          </div>
        </div>
        <div class="form-feedback"></div>
      </div>

      <!-- Campo Email -->
      <div class="form-group">
        <label for="email" class="form-label">Dirección de Email</label>
        <div class="input-wrapper">
          <input type="email" id="email" name="email" class="form-input" required>
          <span class="input-focus-border"></span>
          <div class="input-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
            </svg>
          </div>
        </div>
        <div class="form-feedback"></div>
      </div>
    </div>

    <!-- Campo Teléfono -->
    <div class="form-group">
      <label for="phone" class="form-label">Número de Teléfono</label>
      <div class="input-wrapper">
        <input type="tel" id="phone" name="phone" class="form-input">
        <span class="input-focus-border"></span>
        <div class="input-icon">
          <svg viewBox="0 0 24 24" fill="currentColor">
            <path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
          </svg>
        </div>
      </div>
      <div class="form-feedback"></div>
    </div>

    <!-- Campo Asunto -->
    <div class="form-group">
      <label for="subject" class="form-label">Asunto</label>
      <div class="select-wrapper">
        <select id="subject" name="subject" class="form-select" required>
          <option value="">Elige un asunto</option>
          <option value="general">Consulta General</option>
          <option value="support">Soporte Técnico</option>
          <option value="sales">Pregunta de Ventas</option>
          <option value="feedback">Comentarios</option>
          <option value="other">Otro</option>
        </select>
        <div class="select-icon">
          <svg viewBox="0 0 24 24" fill="currentColor">
            <path d="M7 10l5 5 5-5z"/>
          </svg>
        </div>
      </div>
      <div class="form-feedback"></div>
    </div>

    <!-- Campo Mensaje -->
    <div class="form-group">
      <label for="message" class="form-label">Mensaje</label>
      <div class="textarea-wrapper">
        <textarea id="message" name="message" class="form-textarea" rows="5" required placeholder="Cuéntanos más sobre tu consulta..."></textarea>
        <span class="input-focus-border"></span>
        <div class="character-count">
          <span class="current-count">0</span>/<span class="max-count">500</span>
        </div>
      </div>
      <div class="form-feedback"></div>
    </div>

    <!-- Carga de Archivos -->
    <div class="form-group">
      <label for="attachment" class="form-label">Adjunto (Opcional)</label>
      <div class="file-upload-wrapper">
        <input type="file" id="attachment" name="attachment" class="file-input" accept=".pdf,.doc,.docx,.jpg,.png" multiple>
        <div class="file-upload-area">
          <div class="file-upload-icon">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
            </svg>
          </div>
          <div class="file-upload-text">
            <span class="file-upload-title">Arrastra archivos aquí o haz clic para explorar</span>
            <span class="file-upload-subtitle">PDF, DOC, JPG, PNG hasta 10MB</span>
          </div>
        </div>
        <div class="file-list"></div>
      </div>
      <div class="form-feedback"></div>
    </div>

    <!-- Checkbox -->
    <div class="form-group">
      <div class="checkbox-wrapper">
        <input type="checkbox" id="newsletter" name="newsletter" class="form-checkbox">
        <label for="newsletter" class="checkbox-label">
          <span class="checkbox-custom">
            <svg class="checkbox-icon" 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>
          Suscribirse a nuestro boletín para actualizaciones y noticias
        </label>
      </div>
    </div>

    <!-- Política de Privacidad -->
    <div class="form-group">
      <div class="checkbox-wrapper">
        <input type="checkbox" id="privacy" name="privacy" class="form-checkbox" required>
        <label for="privacy" class="checkbox-label">
          <span class="checkbox-custom">
            <svg class="checkbox-icon" 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>
          Acepto la <a href="#" class="privacy-link">Política de Privacidad</a> y los <a href="#" class="privacy-link">Términos de Servicio</a>
        </label>
      </div>
      <div class="form-feedback"></div>
    </div>

    <!-- Botón de Envío -->
    <div class="form-actions">
      <button type="submit" class="btn-submit">
        <span class="btn-text">Enviar Mensaje</span>
        <span class="btn-loading">
          <svg class="loading-spinner" viewBox="0 0 24 24">
            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="32" stroke-dashoffset="32">
              <animate attributeName="stroke-dasharray" dur="2s" values="0 32;16 16;0 32;0 32" repeatCount="indefinite"/>
              <animate attributeName="stroke-dashoffset" dur="2s" values="0;-16;-32;-32" repeatCount="indefinite"/>
            </circle>
          </svg>
        </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>
    </div>
  </form>

  <!-- Formulario Multi-paso -->
  <form class="interactive-form multi-step-form" id="multiStepForm">
    <div class="form-header">
      <h2 class="form-title">Crear Cuenta</h2>
      <div class="progress-bar">
        <div class="progress-step active" data-step="1">
          <div class="step-number">1</div>
          <div class="step-label">Info Personal</div>
        </div>
        <div class="progress-step" data-step="2">
          <div class="step-number">2</div>
          <div class="step-label">Detalles de Cuenta</div>
        </div>
        <div class="progress-step" data-step="3">
          <div class="step-number">3</div>
          <div class="step-label">Preferencias</div>
        </div>
        <div class="progress-step" data-step="4">
          <div class="step-number">4</div>
          <div class="step-label">Confirmación</div>
        </div>
      </div>
    </div>

    <!-- Paso 1: Información Personal -->
    <div class="form-step active" data-step="1">
      <div class="form-grid">
        <div class="form-group">
          <label for="firstName" class="form-label">Nombre</label>
          <div class="input-wrapper">
            <input type="text" id="firstName" name="firstName" class="form-input" required>
            <span class="input-focus-border"></span>
          </div>
          <div class="form-feedback"></div>
        </div>
        <div class="form-group">
          <label for="lastName" class="form-label">Apellido</label>
          <div class="input-wrapper">
            <input type="text" id="lastName" name="lastName" class="form-input" required>
            <span class="input-focus-border"></span>
          </div>
          <div class="form-feedback"></div>
        </div>
      </div>
      <div class="form-group">
        <label for="birthDate" class="form-label">Fecha de Nacimiento</label>
        <div class="input-wrapper">
          <input type="date" id="birthDate" name="birthDate" class="form-input" required>
          <span class="input-focus-border"></span>
        </div>
        <div class="form-feedback"></div>
      </div>
    </div>

    <!-- Paso 2: Detalles de Cuenta -->
    <div class="form-step" data-step="2">
      <div class="form-group">
        <label for="username" class="form-label">Nombre de Usuario</label>
        <div class="input-wrapper">
          <input type="text" id="username" name="username" class="form-input" required>
          <span class="input-focus-border"></span>
          <div class="input-validation-icon"></div>
        </div>
        <div class="form-feedback"></div>
      </div>
      <div class="form-group">
        <label for="password" class="form-label">Contraseña</label>
        <div class="input-wrapper">
          <input type="password" id="password" name="password" class="form-input" required>
          <span class="input-focus-border"></span>
          <button type="button" class="password-toggle" aria-label="Alternar visibilidad de contraseña">
            <svg class="eye-open" viewBox="0 0 24 24" fill="currentColor">
              <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
            </svg>
            <svg class="eye-closed" viewBox="0 0 24 24" fill="currentColor" style="display: none;">
              <path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>
            </svg>
          </button>
        </div>
        <div class="password-strength">
          <div class="strength-bar">
            <div class="strength-fill"></div>
          </div>
          <div class="strength-text">Fuerza de contraseña: <span class="strength-level">Débil</span></div>
        </div>
        <div class="form-feedback"></div>
      </div>
    </div>

    <!-- Paso 3: Preferencias -->
    <div class="form-step" data-step="3">
      <div class="form-group">
        <label class="form-label">Preferencias de Notificación</label>
        <div class="radio-group">
          <div class="radio-wrapper">
            <input type="radio" id="emailNotif" name="notifications" value="email" class="form-radio">
            <label for="emailNotif" class="radio-label">
              <span class="radio-custom"></span>
              Solo notificaciones por email
            </label>
          </div>
          <div class="radio-wrapper">
            <input type="radio" id="smsNotif" name="notifications" value="sms" class="form-radio">
            <label for="smsNotif" class="radio-label">
              <span class="radio-custom"></span>
              Solo notificaciones por SMS
            </label>
          </div>
          <div class="radio-wrapper">
            <input type="radio" id="bothNotif" name="notifications" value="both" class="form-radio" checked>
            <label for="bothNotif" class="radio-label">
              <span class="radio-custom"></span>
              Ambos email y SMS
            </label>
          </div>
        </div>
      </div>
    </div>

    <!-- Paso 4: Confirmación -->
    <div class="form-step" data-step="4">
      <div class="confirmation-content">
        <div class="success-icon">
          <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>
        <h3>¡Cuenta Creada Exitosamente!</h3>
        <p>Bienvenido a nuestra plataforma. Tu cuenta ha sido creada y ahora puedes comenzar a usar nuestros servicios.</p>
      </div>
    </div>

    <!-- Botones de Navegación -->
    <div class="form-navigation">
      <button type="button" class="btn-secondary btn-prev" disabled>
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
        </svg>
        Anterior
      </button>
      <button type="button" class="btn-primary btn-next">
        Siguiente
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/>
        </svg>
      </button>
      <button type="submit" class="btn-primary btn-submit" style="display: none;">
        Crear Cuenta
      </button>
    </div>
  </form>
</div>
.form-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  gap: 3rem;
}

/* Estilos Base del Formulario */
.interactive-form {
  background: white;
  border-radius: 16px;
  padding: 2rem;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  border: 1px solid #e5e7eb;
}

.form-header {
  text-align: center;
  margin-bottom: 2rem;
}

.form-title {
  font-size: 2rem;
  font-weight: 700;
  color: #1a1a1a;
  margin: 0 0 0.5rem 0;
}

.form-description {
  color: #6b7280;
  font-size: 1rem;
  margin: 0;
  line-height: 1.6;
}

/* Grid del Formulario */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
}

/* Grupos de Formulario */
.form-group {
  margin-bottom: 1.5rem;
}

.form-label {
  display: block;
  font-weight: 600;
  color: #374151;
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Wrapper de Input */
.input-wrapper {
  position: relative;
}

.form-input {
  width: 100%;
  padding: 1rem 1rem 1rem 3rem;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  font-size: 1rem;
  transition: all 0.3s ease;
  background: white;
  outline: none;
}

.form-input:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.form-input:valid {
  border-color: #10b981;
}

.form-input:invalid:not(:placeholder-shown) {
  border-color: #ef4444;
}

.input-focus-border {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0;
  height: 2px;
  background: linear-gradient(90deg, #3b82f6, #1d4ed8);
  transition: width 0.3s ease;
}

.form-input:focus + .input-focus-border {
  width: 100%;
}

.input-icon {
  position: absolute;
  left: 1rem;
  top: 50%;
  transform: translateY(-50%);
  color: #9ca3af;
  transition: color 0.3s ease;
}

.input-icon svg {
  width: 20px;
  height: 20px;
}

.form-input:focus ~ .input-icon {
  color: #3b82f6;
}

/* Estilos de Select */
.select-wrapper {
  position: relative;
}

.form-select {
  width: 100%;
  padding: 1rem 3rem 1rem 1rem;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  font-size: 1rem;
  background: white;
  cursor: pointer;
  outline: none;
  appearance: none;
  transition: all 0.3s ease;
}

.form-select:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.select-icon {
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  color: #9ca3af;
  pointer-events: none;
  transition: transform 0.3s ease;
}

.select-icon svg {
  width: 20px;
  height: 20px;
}

.form-select:focus ~ .select-icon {
  transform: translateY(-50%) rotate(180deg);
  color: #3b82f6;
}

/* Estilos de Textarea */
.textarea-wrapper {
  position: relative;
}

.form-textarea {
  width: 100%;
  padding: 1rem;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  font-size: 1rem;
  font-family: inherit;
  resize: vertical;
  min-height: 120px;
  outline: none;
  transition: all 0.3s ease;
}

.form-textarea:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.character-count {
  position: absolute;
  bottom: 0.5rem;
  right: 1rem;
  font-size: 0.75rem;
  color: #9ca3af;
  background: white;
  padding: 0.25rem;
}

/* Carga de Archivos */
.file-upload-wrapper {
  position: relative;
}

.file-input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.file-upload-area {
  border: 2px dashed #d1d5db;
  border-radius: 8px;
  padding: 2rem;
  text-align: center;
  transition: all 0.3s ease;
  cursor: pointer;
}

.file-upload-area:hover,
.file-upload-area.dragover {
  border-color: #3b82f6;
  background: rgba(59, 130, 246, 0.05);
}

.file-upload-icon {
  width: 48px;
  height: 48px;
  margin: 0 auto 1rem;
  color: #9ca3af;
}

.file-upload-icon svg {
  width: 100%;
  height: 100%;
}

.file-upload-title {
  display: block;
  font-weight: 600;
  color: #374151;
  margin-bottom: 0.25rem;
}

.file-upload-subtitle {
  display: block;
  font-size: 0.875rem;
  color: #9ca3af;
}

.file-list {
  margin-top: 1rem;
}

.file-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem;
  background: #f9fafb;
  border-radius: 6px;
  margin-bottom: 0.5rem;
}

.file-info {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.file-icon {
  width: 24px;
  height: 24px;
  color: #6b7280;
}

.file-details {
  display: flex;
  flex-direction: column;
}

.file-name {
  font-weight: 500;
  color: #374151;
  font-size: 0.875rem;
}

.file-size {
  font-size: 0.75rem;
  color: #9ca3af;
}

.file-remove {
  background: none;
  border: none;
  color: #ef4444;
  cursor: pointer;
  padding: 0.25rem;
  border-radius: 4px;
  transition: background 0.3s ease;
}

.file-remove:hover {
  background: rgba(239, 68, 68, 0.1);
}

/* Estilos de Checkbox */
.checkbox-wrapper {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
}

.form-checkbox {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

.checkbox-custom {
  width: 20px;
  height: 20px;
  border: 2px solid #d1d5db;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  flex-shrink: 0;
  margin-top: 0.125rem;
}

.checkbox-icon {
  width: 14px;
  height: 14px;
  color: white;
  opacity: 0;
  transform: scale(0.5);
  transition: all 0.3s ease;
}

.form-checkbox:checked + .checkbox-label .checkbox-custom {
  background: #3b82f6;
  border-color: #3b82f6;
}

.form-checkbox:checked + .checkbox-label .checkbox-icon {
  opacity: 1;
  transform: scale(1);
}

.checkbox-label {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  cursor: pointer;
  font-size: 0.875rem;
  line-height: 1.5;
  color: #374151;
}

.privacy-link {
  color: #3b82f6;
  text-decoration: none;
  font-weight: 500;
}

.privacy-link:hover {
  text-decoration: underline;
}

/* Estilos de Radio */
.radio-group {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.radio-wrapper {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.form-radio {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

.radio-custom {
  width: 20px;
  height: 20px;
  border: 2px solid #d1d5db;
  border-radius: 50%;
  position: relative;
  transition: all 0.3s ease;
  flex-shrink: 0;
}

.radio-custom::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0);
  width: 8px;
  height: 8px;
  background: #3b82f6;
  border-radius: 50%;
  transition: transform 0.3s ease;
}

.form-radio:checked + .radio-label .radio-custom {
  border-color: #3b82f6;
}

.form-radio:checked + .radio-label .radio-custom::after {
  transform: translate(-50%, -50%) scale(1);
}

.radio-label {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  cursor: pointer;
  font-size: 0.875rem;
  color: #374151;
}

/* Alternar Contraseña */
.password-toggle {
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: #9ca3af;
  cursor: pointer;
  padding: 0.25rem;
  border-radius: 4px;
  transition: color 0.3s ease;
}

.password-toggle:hover {
  color: #6b7280;
}

.password-toggle svg {
  width: 20px;
  height: 20px;
}

/* Fuerza de Contraseña */
.password-strength {
  margin-top: 0.5rem;
}

.strength-bar {
  width: 100%;
  height: 4px;
  background: #e5e7eb;
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 0.5rem;
}

.strength-fill {
  height: 100%;
  width: 0;
  transition: all 0.3s ease;
  border-radius: 2px;
}

.strength-fill.weak {
  width: 25%;
  background: #ef4444;
}

.strength-fill.fair {
  width: 50%;
  background: #f59e0b;
}

.strength-fill.good {
  width: 75%;
  background: #3b82f6;
}

.strength-fill.strong {
  width: 100%;
  background: #10b981;
}

.strength-text {
  font-size: 0.75rem;
  color: #6b7280;
}

.strength-level {
  font-weight: 600;
}

/* Retroalimentación del Formulario */
.form-feedback {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  min-height: 1.25rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.form-feedback.error {
  color: #ef4444;
}

.form-feedback.success {
  color: #10b981;
}

.form-feedback.warning {
  color: #f59e0b;
}

.feedback-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* Botones */
.form-actions {
  margin-top: 2rem;
}

.btn-submit {
  width: 100%;
  padding: 1rem 2rem;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.btn-submit:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 20px rgba(59, 130, 246, 0.3);
}

.btn-submit:disabled {
  opacity: 0.7;
  cursor: not-allowed;
  transform: none;
}

.btn-text,
.btn-loading,
.btn-success {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  transition: all 0.3s ease;
}

.btn-loading,
.btn-success {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0;
  transform: translateY(100%);
}

.btn-submit.loading .btn-text {
  opacity: 0;
  transform: translateY(-100%);
}

.btn-submit.loading .btn-loading {
  opacity: 1;
  transform: translateY(0);
}

.btn-submit.success .btn-text,
.btn-submit.success .btn-loading {
  opacity: 0;
  transform: translateY(-100%);
}

.btn-submit.success .btn-success {
  opacity: 1;
  transform: translateY(0);
}

.loading-spinner {
  width: 20px;
  height: 20px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Formulario Multi-paso */
.progress-bar {
  display: flex;
  justify-content: space-between;
  margin: 2rem 0;
  position: relative;
}

.progress-bar::before {
  content: '';
  position: absolute;
  top: 20px;
  left: 0;
  right: 0;
  height: 2px;
  background: #e5e7eb;
  z-index: 1;
}

.progress-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  z-index: 2;
}

.step-number {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #e5e7eb;
  color: #9ca3af;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  transition: all 0.3s ease;
  margin-bottom: 0.5rem;
}

.progress-step.active .step-number {
  background: #3b82f6;
  color: white;
}

.progress-step.completed .step-number {
  background: #10b981;
  color: white;
}

.step-label {
  font-size: 0.75rem;
  color: #6b7280;
  text-align: center;
  font-weight: 500;
}

.progress-step.active .step-label {
  color: #3b82f6;
  font-weight: 600;
}

.form-step {
  display: none;
  animation: fadeInSlide 0.3s ease;
}

.form-step.active {
  display: block;
}

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

.form-navigation {
  display: flex;
  justify-content: space-between;
  margin-top: 2rem;
  gap: 1rem;
}

.btn-secondary {
  padding: 0.75rem 1.5rem;
  background: #f3f4f6;
  color: #374151;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.btn-secondary:hover:not(:disabled) {
  background: #e5e7eb;
}

.btn-secondary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  padding: 0.75rem 1.5rem;
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
  color: white;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.btn-primary:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}

.btn-primary svg {
  width: 16px;
  height: 16px;
}

/* Paso de Confirmación */
.confirmation-content {
  text-align: center;
  padding: 2rem 0;
}

.success-icon {
  width: 80px;
  height: 80px;
  background: #10b981;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 1.5rem;
  color: white;
}

.success-icon svg {
  width: 40px;
  height: 40px;
}

.confirmation-content h3 {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a1a;
  margin: 0 0 1rem 0;
}

.confirmation-content p {
  color: #6b7280;
  line-height: 1.6;
  margin: 0;
}

/* Tema Oscuro */
.dark .interactive-form {
  background: #1f2937;
  border-color: #374151;
}

.dark .form-title {
  color: white;
}

.dark .form-description {
  color: #d1d5db;
}

.dark .form-label {
  color: #e5e7eb;
}

.dark .form-input,
.dark .form-select,
.dark .form-textarea {
  background: #374151;
  border-color: #4b5563;
  color: white;
}

.dark .form-input:focus,
.dark .form-select:focus,
.dark .form-textarea:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.dark .file-upload-area {
  border-color: #4b5563;
  background: #374151;
}

.dark .file-upload-area:hover {
  border-color: #3b82f6;
  background: rgba(59, 130, 246, 0.1);
}

.dark .checkbox-custom,
.dark .radio-custom {
  border-color: #4b5563;
  background: #374151;
}

.dark .step-number {
  background: #374151;
  color: #d1d5db;
}

/* Diseño Responsivo */
@media (max-width: 768px) {
  .form-container {
    padding: 1rem;
  }
  
  .interactive-form {
    padding: 1.5rem;
  }
  
  .form-grid {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
  
  .form-title {
    font-size: 1.5rem;
  }
  
  .progress-bar {
    flex-wrap: wrap;
    gap: 1rem;
  }
  
  .step-label {
    display: none;
  }
  
  .form-navigation {
    flex-direction: column;
  }
}

@media (max-width: 480px) {
  .form-input {
    padding: 0.875rem 0.875rem 0.875rem 2.5rem;
  }
  
  .input-icon {
    left: 0.75rem;
  }
  
  .input-icon svg {
    width: 18px;
    height: 18px;
  }
  
  .btn-submit {
    padding: 0.875rem 1.5rem;
  }
}
class FormulariosInteractivos {
  constructor() {
    this.formularios = document.querySelectorAll('.interactive-form');
    this.reglasValidacion = {
      email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
      telefono: /^[\+]?[1-9][\d]{0,2}[\s\-]?[\(]?[\d]{1,3}[\)]?[\s\-]?[\d]{3,4}[\s\-]?[\d]{3,4}$/,
      contrasena: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
    };
    
    this.init();
  }

  init() {
    this.formularios.forEach(formulario => {
      this.configurarValidacionFormulario(formulario);
      this.configurarCargaArchivos(formulario);
      this.configurarAlternarContrasena(formulario);
      this.configurarContadorCaracteres(formulario);
      this.configurarMultiPaso(formulario);
    });
  }

  configurarValidacionFormulario(formulario) {
    const inputs = formulario.querySelectorAll('.form-input, .form-select, .form-textarea');
    
    inputs.forEach(input => {
      // Validación en tiempo real
      input.addEventListener('input', () => {
        this.validarCampo(input);
      });
      
      input.addEventListener('blur', () => {
        this.validarCampo(input);
      });
      
      // Verificación de disponibilidad de nombre de usuario
      if (input.name === 'username') {
        let temporizadorDebounce;
        input.addEventListener('input', () => {
          clearTimeout(temporizadorDebounce);
          temporizadorDebounce = setTimeout(() => {
            this.verificarDisponibilidadUsuario(input);
          }, 500);
        });
      }
      
      // Fuerza de contraseña
      if (input.type === 'password') {
        input.addEventListener('input', () => {
          this.actualizarFuerzaContrasena(input);
        });
      }
    });
    
    // Envío de formulario
    formulario.addEventListener('submit', (e) => {
      e.preventDefault();
      this.manejarEnvioFormulario(formulario);
    });
  }

  validarCampo(campo) {
    const valor = campo.value.trim();
    const nombreCampo = campo.name;
    const retroalimentacion = campo.closest('.form-group').querySelector('.form-feedback');
    
    let esValido = true;
    let mensaje = '';
    let tipo = 'success';
    
    // Validación de campo requerido
    if (campo.hasAttribute('required') && !valor) {
      esValido = false;
      mensaje = 'Este campo es requerido';
      tipo = 'error';
    }
    
    // Validación de email
    else if (campo.type === 'email' && valor && !this.reglasValidacion.email.test(valor)) {
      esValido = false;
      mensaje = 'Por favor ingresa una dirección de email válida';
      tipo = 'error';
    }
    
    // Validación de teléfono
    else if (campo.type === 'tel' && valor && !this.reglasValidacion.telefono.test(valor)) {
      esValido = false;
      mensaje = 'Por favor ingresa un número de teléfono válido';
      tipo = 'error';
    }
    
    // Validación de contraseña
    else if (campo.type === 'password' && valor && !this.reglasValidacion.contrasena.test(valor)) {
      esValido = false;
      mensaje = 'La contraseña debe tener al menos 8 caracteres con mayúscula, minúscula, número y carácter especial';
      tipo = 'error';
    }
    
    // Mensaje de éxito
    else if (valor && campo.checkValidity()) {
      mensaje = '¡Se ve bien!';
      tipo = 'success';
    }
    
    this.mostrarRetroalimentacionCampo(retroalimentacion, mensaje, tipo);
    
    // Actualizar estilo del campo
    campo.classList.toggle('valid', esValido && valor);
    campo.classList.toggle('invalid', !esValido && valor);
    
    return esValido;
  }

  mostrarRetroalimentacionCampo(elementoRetroalimentacion, mensaje, tipo) {
    elementoRetroalimentacion.className = `form-feedback ${tipo}`;
    
    const icono = this.obtenerIconoValidacion(tipo);
    elementoRetroalimentacion.innerHTML = mensaje ? `${icono}<span>${mensaje}</span>` : '';
  }

  obtenerIconoValidacion(tipo) {
    const iconos = {
      success: '<svg class="feedback-icon" 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>',
      error: '<svg class="feedback-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
      warning: '<svg class="feedback-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>'
    };
    return iconos[tipo] || '';
  }

  async verificarDisponibilidadUsuario(input) {
    const nombreUsuario = input.value.trim();
    if (nombreUsuario.length < 3) return;
    
    const wrapper = input.closest('.input-wrapper');
    const iconoValidacion = wrapper.querySelector('.input-validation-icon');
    
    if (!iconoValidacion) {
      const icono = document.createElement('div');
      icono.className = 'input-validation-icon';
      wrapper.appendChild(icono);
    }
    
    // Simular llamada a API
    try {
      const estaDisponible = await this.simularVerificacionUsuario(nombreUsuario);
      const retroalimentacion = input.closest('.form-group').querySelector('.form-feedback');
      
      if (estaDisponible) {
        this.mostrarRetroalimentacionCampo(retroalimentacion, 'Nombre de usuario disponible', 'success');
        input.classList.add('valid');
        input.classList.remove('invalid');
      } else {
        this.mostrarRetroalimentacionCampo(retroalimentacion, 'Nombre de usuario ya está en uso', 'error');
        input.classList.add('invalid');
        input.classList.remove('valid');
      }
    } catch (error) {
      console.error('Verificación de usuario falló:', error);
    }
  }

  simularVerificacionUsuario(nombreUsuario) {
    return new Promise((resolve) => {
      setTimeout(() => {
        // Simular algunos nombres de usuario ocupados
        const usuariosOcupados = ['admin', 'usuario', 'test', 'demo'];
        resolve(!usuariosOcupados.includes(nombreUsuario.toLowerCase()));
      }, 1000);
    });
  }

  actualizarFuerzaContrasena(input) {
    const contrasena = input.value;
    const barraFuerza = input.closest('.form-group').querySelector('.strength-fill');
    const textoFuerza = input.closest('.form-group').querySelector('.strength-level');
    
    if (!barraFuerza || !textoFuerza) return;
    
    const fuerza = this.calcularFuerzaContrasena(contrasena);
    
    barraFuerza.className = `strength-fill ${fuerza.nivel}`;
    textoFuerza.textContent = fuerza.texto;
  }

  calcularFuerzaContrasena(contrasena) {
    let puntuacion = 0;
    
    if (contrasena.length >= 8) puntuacion++;
    if (/[a-z]/.test(contrasena)) puntuacion++;
    if (/[A-Z]/.test(contrasena)) puntuacion++;
    if (/\d/.test(contrasena)) puntuacion++;
    if (/[@$!%*?&]/.test(contrasena)) puntuacion++;
    
    const niveles = {
      0: { nivel: 'weak', texto: 'Muy Débil' },
      1: { nivel: 'weak', texto: 'Débil' },
      2: { nivel: 'fair', texto: 'Regular' },
      3: { nivel: 'good', texto: 'Buena' },
      4: { nivel: 'strong', texto: 'Fuerte' },
      5: { nivel: 'strong', texto: 'Muy Fuerte' }
    };
    
    return niveles[puntuacion] || niveles[0];
  }

  configurarCargaArchivos(formulario) {
    const inputsArchivo = formulario.querySelectorAll('.file-input');
    
    inputsArchivo.forEach(input => {
      const areaSubida = input.closest('.file-upload-wrapper').querySelector('.file-upload-area');
      const listaArchivos = input.closest('.file-upload-wrapper').querySelector('.file-list');
      
      // Arrastrar y soltar
      areaSubida.addEventListener('dragover', (e) => {
        e.preventDefault();
        areaSubida.classList.add('dragover');
      });
      
      areaSubida.addEventListener('dragleave', () => {
        areaSubida.classList.remove('dragover');
      });
      
      areaSubida.addEventListener('drop', (e) => {
        e.preventDefault();
        areaSubida.classList.remove('dragover');
        
        const archivos = Array.from(e.dataTransfer.files);
        this.manejarSeleccionArchivos(archivos, listaArchivos);
      });
      
      // Selección de archivos
      input.addEventListener('change', (e) => {
        const archivos = Array.from(e.target.files);
        this.manejarSeleccionArchivos(archivos, listaArchivos);
      });
    });
  }

  manejarSeleccionArchivos(archivos, listaArchivos) {
    archivos.forEach(archivo => {
      if (this.validarArchivo(archivo)) {
        this.agregarArchivoALista(archivo, listaArchivos);
      }
    });
  }

  validarArchivo(archivo) {
    const tamanoMaximo = 10 * 1024 * 1024; // 10MB
    const tiposPermitidos = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/jpeg', 'image/png'];
    
    if (archivo.size > tamanoMaximo) {
      alert(`El archivo ${archivo.name} es muy grande. El tamaño máximo es 10MB.`);
      return false;
    }
    
    if (!tiposPermitidos.includes(archivo.type)) {
      alert(`El archivo ${archivo.name} no es un formato soportado.`);
      return false;
    }
    
    return true;
  }

  agregarArchivoALista(archivo, listaArchivos) {
    const elementoArchivo = document.createElement('div');
    elementoArchivo.className = 'file-item';
    
    elementoArchivo.innerHTML = `
      <div class="file-info">
        <div class="file-icon">
          <svg viewBox="0 0 24 24" fill="currentColor">
            <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
          </svg>
        </div>
        <div class="file-details">
          <div class="file-name">${archivo.name}</div>
          <div class="file-size">${this.formatearTamanoArchivo(archivo.size)}</div>
        </div>
      </div>
      <button type="button" class="file-remove" aria-label="Remover archivo">
        <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>
      </button>
    `;
    
    listaArchivos.appendChild(elementoArchivo);
    
    // Evento para remover archivo
    elementoArchivo.querySelector('.file-remove').addEventListener('click', () => {
      elementoArchivo.remove();
    });
  }

  formatearTamanoArchivo(bytes) {
    if (bytes === 0) return '0 Bytes';
    
    const k = 1024;
    const tamaños = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + tamaños[i];
  }

  configurarAlternarContrasena(formulario) {
    const botonesAlternar = formulario.querySelectorAll('.password-toggle');
    
    botonesAlternar.forEach(boton => {
      boton.addEventListener('click', () => {
        const input = boton.closest('.input-wrapper').querySelector('input');
        const iconoAbierto = boton.querySelector('.eye-open');
        const iconoCerrado = boton.querySelector('.eye-closed');
        
        if (input.type === 'password') {
          input.type = 'text';
          iconoAbierto.style.display = 'none';
          iconoCerrado.style.display = 'block';
        } else {
          input.type = 'password';
          iconoAbierto.style.display = 'block';
          iconoCerrado.style.display = 'none';
        }
      });
    });
  }

  configurarContadorCaracteres(formulario) {
    const textareas = formulario.querySelectorAll('.form-textarea');
    
    textareas.forEach(textarea => {
      const contadorActual = textarea.closest('.textarea-wrapper').querySelector('.current-count');
      const contadorMaximo = textarea.closest('.textarea-wrapper').querySelector('.max-count');
      
      if (contadorActual && contadorMaximo) {
        const limite = parseInt(contadorMaximo.textContent);
        
        textarea.addEventListener('input', () => {
          const longitudActual = textarea.value.length;
          contadorActual.textContent = longitudActual;
          
          // Cambiar color cuando se acerca al límite
          const porcentaje = (longitudActual / limite) * 100;
          if (porcentaje >= 90) {
            contadorActual.style.color = '#ef4444';
          } else if (porcentaje >= 75) {
            contadorActual.style.color = '#f59e0b';
          } else {
            contadorActual.style.color = '#9ca3af';
          }
          
          // Prevenir exceder el límite
          if (longitudActual > limite) {
            textarea.value = textarea.value.substring(0, limite);
            contadorActual.textContent = limite;
          }
        });
      }
    });
  }

  configurarMultiPaso(formulario) {
    if (!formulario.classList.contains('multi-step-form')) return;
    
    const pasos = formulario.querySelectorAll('.form-step');
    const pasosProgreso = formulario.querySelectorAll('.progress-step');
    const botonAnterior = formulario.querySelector('.btn-prev');
    const botonSiguiente = formulario.querySelector('.btn-next');
    const botonEnviar = formulario.querySelector('.btn-submit');
    
    let pasoActual = 1;
    
    // Navegación de pasos
    botonSiguiente.addEventListener('click', () => {
      if (this.validarPasoActual(formulario, pasoActual)) {
        this.irAlPaso(pasoActual + 1, pasos, pasosProgreso, botonAnterior, botonSiguiente, botonEnviar);
        pasoActual++;
      }
    });
    
    botonAnterior.addEventListener('click', () => {
      this.irAlPaso(pasoActual - 1, pasos, pasosProgreso, botonAnterior, botonSiguiente, botonEnviar);
      pasoActual--;
    });
  }

  validarPasoActual(formulario, numeroPaso) {
    const pasoActual = formulario.querySelector(`[data-step="${numeroPaso}"]`);
    const camposRequeridos = pasoActual.querySelectorAll('[required]');
    
    let esValido = true;
    
    camposRequeridos.forEach(campo => {
      if (!this.validarCampo(campo)) {
        esValido = false;
      }
    });
    
    return esValido;
  }

  irAlPaso(numeroPaso, pasos, pasosProgreso, botonAnterior, botonSiguiente, botonEnviar) {
    // Actualizar pasos visibles
    pasos.forEach((paso, indice) => {
      paso.classList.toggle('active', indice + 1 === numeroPaso);
    });
    
    // Actualizar progreso
    pasosProgreso.forEach((paso, indice) => {
      const numeroEtapa = indice + 1;
      paso.classList.toggle('active', numeroEtapa === numeroPaso);
      paso.classList.toggle('completed', numeroEtapa < numeroPaso);
    });
    
    // Actualizar botones
    botonAnterior.disabled = numeroPaso === 1;
    
    if (numeroPaso === pasos.length) {
      botonSiguiente.style.display = 'none';
      botonEnviar.style.display = 'flex';
    } else {
      botonSiguiente.style.display = 'flex';
      botonEnviar.style.display = 'none';
    }
  }

  async manejarEnvioFormulario(formulario) {
    const botonEnviar = formulario.querySelector('.btn-submit');
    const textoBoton = botonEnviar.querySelector('.btn-text');
    const cargandoBoton = botonEnviar.querySelector('.btn-loading');
    const exitoBoton = botonEnviar.querySelector('.btn-success');
    
    // Validar todos los campos
    const camposRequeridos = formulario.querySelectorAll('[required]');
    let formularioValido = true;
    
    camposRequeridos.forEach(campo => {
      if (!this.validarCampo(campo)) {
        formularioValido = false;
      }
    });
    
    if (!formularioValido) {
      this.mostrarMensajeFormulario(formulario, 'Por favor corrige los errores antes de enviar', 'error');
      return;
    }
    
    // Estado de carga
    botonEnviar.disabled = true;
    botonEnviar.classList.add('loading');
    
    try {
      // Simular envío de formulario
      const datosFormulario = this.obtenerDatosFormulario(formulario);
      await this.simularEnvioAPI(datosFormulario);
      
      // Estado de éxito
      botonEnviar.classList.remove('loading');
      botonEnviar.classList.add('success');
      
      this.mostrarMensajeFormulario(formulario, '¡Formulario enviado exitosamente!', 'success');
      
      // Resetear formulario después de un retraso
      setTimeout(() => {
        this.resetearFormulario(formulario);
      }, 2000);
      
    } catch (error) {
      // Estado de error
      botonEnviar.classList.remove('loading');
      botonEnviar.disabled = false;
      
      this.mostrarMensajeFormulario(formulario, 'Error al enviar el formulario. Por favor intenta de nuevo.', 'error');
      console.error('Error de envío:', error);
    }
  }

  mostrarMensajeFormulario(formulario, mensaje, tipo) {
    let contenedorMensaje = formulario.querySelector('.form-message');
    
    if (!contenedorMensaje) {
      contenedorMensaje = document.createElement('div');
      contenedorMensaje.className = 'form-message';
      formulario.insertBefore(contenedorMensaje, formulario.querySelector('.form-actions'));
    }
    
    contenedorMensaje.className = `form-message ${tipo}`;
    contenedorMensaje.innerHTML = `
      <div class="message-content">
        ${this.obtenerIconoValidacion(tipo)}
        <span>${mensaje}</span>
      </div>
    `;
    
    // Auto-ocultar después de 5 segundos
    setTimeout(() => {
      contenedorMensaje.remove();
    }, 5000);
  }

  resetearFormulario(formulario) {
    formulario.reset();
    
    // Limpiar estados de validación
    const campos = formulario.querySelectorAll('.form-input, .form-select, .form-textarea');
    campos.forEach(campo => {
      campo.classList.remove('valid', 'invalid');
    });
    
    // Limpiar retroalimentación
    const retroalimentaciones = formulario.querySelectorAll('.form-feedback');
    retroalimentaciones.forEach(retroalimentacion => {
      retroalimentacion.innerHTML = '';
      retroalimentacion.className = 'form-feedback';
    });
    
    // Resetear botón
    const botonEnviar = formulario.querySelector('.btn-submit');
    botonEnviar.disabled = false;
    botonEnviar.classList.remove('loading', 'success');
    
    // Resetear formulario multi-paso
    if (formulario.classList.contains('multi-step-form')) {
      this.resetearMultiPaso(formulario);
    }
  }

  resetearMultiPaso(formulario) {
    const pasos = formulario.querySelectorAll('.form-step');
    const pasosProgreso = formulario.querySelectorAll('.progress-step');
    const botonAnterior = formulario.querySelector('.btn-prev');
    const botonSiguiente = formulario.querySelector('.btn-next');
    const botonEnviar = formulario.querySelector('.btn-submit');
    
    this.irAlPaso(1, pasos, pasosProgreso, botonAnterior, botonSiguiente, botonEnviar);
  }

  simularEnvioAPI(datos) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // Simular éxito/fallo aleatorio
        if (Math.random() > 0.1) {
          resolve({ exito: true, mensaje: 'Formulario enviado exitosamente' });
        } else {
          reject(new Error('Error del servidor'));
        }
      }, 2000);
    });
  }

  // API Pública
  validarFormulario(idFormulario) {
    const formulario = document.getElementById(idFormulario);
    if (!formulario) return false;
    
    const camposRequeridos = formulario.querySelectorAll('[required]');
    let esValido = true;
    
    camposRequeridos.forEach(campo => {
      if (!this.validarCampo(campo)) {
        esValido = false;
      }
    });
    
    return esValido;
  }

  obtenerDatosFormulario(formulario) {
    const datosFormulario = new FormData(formulario);
    const datos = {};
    
    for (let [clave, valor] of datosFormulario.entries()) {
      datos[clave] = valor;
    }
    
    return datos;
  }

  establecerDatosFormulario(idFormulario, datos) {
    const formulario = document.getElementById(idFormulario);
    if (!formulario) return;
    
    Object.keys(datos).forEach(clave => {
      const campo = formulario.querySelector(`[name="${clave}"]`);
      if (campo) {
        campo.value = datos[clave];
        this.validarCampo(campo);
      }
    });
  }
}

// Inicializar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', () => {
  new FormulariosInteractivos();
});

Ejemplos de Uso

Formulario de Contacto Básico

<form class="interactive-form" id="contactForm">
  <div class="form-group">
    <label for="name" class="form-label">Nombre</label>
    <div class="input-wrapper">
      <input type="text" id="name" name="name" class="form-input" required>
      <span class="input-focus-border"></span>
    </div>
    <div class="form-feedback"></div>
  </div>
  
  <div class="form-group">
    <label for="email" class="form-label">Email</label>
    <div class="input-wrapper">
      <input type="email" id="email" name="email" class="form-input" required>
      <span class="input-focus-border"></span>
    </div>
    <div class="form-feedback"></div>
  </div>
  
  <button type="submit" class="btn-submit">
    <span class="btn-text">Enviar</span>
    <span class="btn-loading">Enviando...</span>
    <span class="btn-success">¡Enviado!</span>
  </button>
</form>

Registro Multi-paso

<form class="interactive-form multi-step-form" id="registrationForm">
  <div class="progress-bar">
    <div class="progress-step active" data-step="1">
      <div class="step-number">1</div>
      <div class="step-label">Personal</div>
    </div>
    <div class="progress-step" data-step="2">
      <div class="step-number">2</div>
      <div class="step-label">Cuenta</div>
    </div>
  </div>
  
  <div class="form-step active" data-step="1">
    <!-- Campos del paso 1 -->
  </div>
  
  <div class="form-step" data-step="2">
    <!-- Campos del paso 2 -->
  </div>
  
  <div class="form-navigation">
    <button type="button" class="btn-prev">Anterior</button>
    <button type="button" class="btn-next">Siguiente</button>
    <button type="submit" class="btn-submit" style="display: none;">Registrarse</button>
  </div>
</form>

Integración con JavaScript

// Validar formulario programáticamente
const formularios = new FormulariosInteractivos();
const esValido = formularios.validarFormulario('contactForm');

// Obtener datos del formulario
const formulario = document.getElementById('contactForm');
const datos = formularios.obtenerDatosFormulario(formulario);

// Establecer datos del formulario
formularios.establecerDatosFormulario('contactForm', {
  name: 'Juan Pérez',
  email: 'juan@ejemplo.com'
});

Referencia de API

Métodos

  • validarFormulario(idFormulario) - Valida todos los campos requeridos
  • obtenerDatosFormulario(formulario) - Obtiene todos los datos del formulario
  • establecerDatosFormulario(idFormulario, datos) - Establece valores en el formulario
  • resetearFormulario(formulario) - Resetea el formulario a su estado inicial

Eventos

  • input - Validación en tiempo real mientras se escribe
  • blur - Validación cuando el campo pierde el foco
  • submit - Manejo del envío del formulario
  • change - Para selects y checkboxes

Opciones de Personalización

Variables CSS

:root {
  --form-primary-color: #3b82f6;
  --form-success-color: #10b981;
  --form-error-color: #ef4444;
  --form-warning-color: #f59e0b;
  --form-border-radius: 8px;
  --form-spacing: 1rem;
}

Reglas de Validación Personalizadas

const formularios = new FormulariosInteractivos();
formularios.reglasValidacion.telefono = /^\+?[1-9]\d{1,14}$/;

Accesibilidad

  • Navegación por Teclado: Soporte completo para Tab, Enter y teclas de flecha
  • Lectores de Pantalla: Etiquetas ARIA y roles semánticos
  • Alto Contraste: Colores que cumplen con WCAG 2.1 AA
  • Indicadores de Foco: Indicadores visuales claros para elementos enfocados

Soporte de Navegadores

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

Consideraciones de Rendimiento

  • Validación Debounced: Evita validaciones excesivas durante la escritura
  • Carga Lazy: Los archivos se procesan solo cuando se seleccionan
  • Animaciones Optimizadas: Usa transform y opacity para animaciones suaves
  • Tamaño Mínimo: CSS y JS optimizados para carga rápida

Ejemplos de Integración

React

import { useEffect } from 'react';

function FormularioContacto() {
  useEffect(() => {
    new FormulariosInteractivos();
  }, []);
  
  return (
    <form className="interactive-form" id="contactForm">
      {/* Campos del formulario */}
    </form>
  );
}

Vue

<template>
  <form class="interactive-form" id="contactForm">
    <!-- Campos del formulario -->
  </form>
</template>

<script>
export default {
  mounted() {
    new FormulariosInteractivos();
  }
}
</script>

HTML

23

líneas

CSS

111

líneas


                <div class="form-container">
  <form class="interactive-form">
    <div class="form-group">
      <input type="text" id="name" required>
      <label for="name">Nombre completo</label>
      <span class="form-line"></span>
    </div>
    <div class="form-group">
      <input type="email" id="email" required>
      <label for="email">Correo electrónico</label>
      <span class="form-line"></span>
    </div>
    <div class="form-group">
      <textarea id="message" required></textarea>
      <label for="message">Mensaje</label>
      <span class="form-line"></span>
    </div>
    <button type="submit" class="submit-btn">
      <span>Enviar mensaje</span>
      <div class="btn-loader"></div>
    </button>
  </form>
</div>

              
23líneas
730caracteres
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 →