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

Componentes de Modales y Popups

Una colección completa de diálogos modales modernos y componentes popup con animaciones suaves, características de accesibilidad y diseños personalizables

#modal #popup #dialog #overlay #components #ui

Diseño Responsivo

Soporte para Modo Oscuro

No

líneas

184

Compatibilidad del Navegador

No

Vista Previa en Vivo

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

600px

Componentes de Modales y Popups

Una colección completa de diálogos modales modernos y componentes popup con animaciones suaves, soporte de accesibilidad, navegación por teclado y diseños personalizables para mejorar la interacción del usuario.

Características

  • Múltiples Tipos de Modal: Modales básicos, diálogos de confirmación, galerías de imágenes y formularios
  • Animaciones Suaves: Transiciones basadas en CSS con temporización personalizable
  • Soporte de Accesibilidad: Atributos ARIA, navegación por teclado y gestión de foco
  • Diseño Responsivo: Se adapta a todos los tamaños de pantalla y orientaciones
  • Control de Fondo: Comportamiento de fondo personalizable y clic para cerrar
  • Soporte de Apilamiento: Múltiples modales con gestión adecuada de z-index
  • Posicionamiento Automático: Posicionamiento inteligente para evitar desbordamiento del viewport
  • Diseño Moderno: Estética limpia y minimalista con efectos sutiles

Demo

<div class="modal-demo-container">
  <!-- Activadores de Modal -->
  <div class="modal-triggers">
    <h3>Tipos de Modal</h3>
    <div class="trigger-grid">
      <button class="trigger-btn" data-modal="basic-modal">
        <span class="trigger-icon">📄</span>
        Modal Básico
      </button>
      
      <button class="trigger-btn" data-modal="confirm-modal">
        <span class="trigger-icon">❓</span>
        Confirmación
      </button>
      
      <button class="trigger-btn" data-modal="form-modal">
        <span class="trigger-icon">📝</span>
        Modal de Formulario
      </button>
      
      <button class="trigger-btn" data-modal="image-modal">
        <span class="trigger-icon">🖼️</span>
        Galería de Imágenes
      </button>
      
      <button class="trigger-btn" data-modal="video-modal">
        <span class="trigger-icon">🎥</span>
        Modal de Video
      </button>
      
      <button class="trigger-btn" data-modal="fullscreen-modal">
        <span class="trigger-icon">⛶</span>
        Pantalla Completa
      </button>
    </div>
  </div>

  <!-- Activadores de Popup -->
  <div class="popup-triggers">
    <h3>Tipos de Popup</h3>
    <div class="trigger-grid">
      <button class="trigger-btn" data-popup="tooltip-popup">
        <span class="trigger-icon">💬</span>
        Tooltip
      </button>
      
      <button class="trigger-btn" data-popup="notification-popup">
        <span class="trigger-icon">🔔</span>
        Notificación
      </button>
      
      <button class="trigger-btn" data-popup="dropdown-popup">
        <span class="trigger-icon">📋</span>
        Desplegable
      </button>
      
      <button class="trigger-btn" data-popup="context-popup">
        <span class="trigger-icon">⚙️</span>
        Menú Contextual
      </button>
    </div>
  </div>
</div>

<!-- Modal Básico -->
<div class="modal-overlay" id="basic-modal">
  <div class="modal-container">
    <div class="modal-header">
      <h2 class="modal-title">Modal Básico</h2>
      <button class="modal-close" aria-label="Cerrar modal">
        <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>
    </div>
    <div class="modal-body">
      <p>Este es un diálogo modal básico con animaciones suaves y características de accesibilidad. Incluye gestión adecuada de foco y soporte de navegación por teclado.</p>
      <p>Puedes personalizar la apariencia, animaciones y comportamiento para que coincidan con tus requisitos de diseño.</p>
    </div>
    <div class="modal-footer">
      <button class="btn btn-secondary modal-cancel">Cancelar</button>
      <button class="btn btn-primary">Confirmar</button>
    </div>
  </div>
</div>

<!-- Modal de Confirmación -->
<div class="modal-overlay" id="confirm-modal">
  <div class="modal-container modal-small">
    <div class="modal-header">
      <div class="modal-icon warning">
        <svg viewBox="0 0 24 24" fill="currentColor">
          <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
        </svg>
      </div>
      <h2 class="modal-title">Confirmar Acción</h2>
      <button class="modal-close" aria-label="Cerrar modal">
        <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>
    </div>
    <div class="modal-body">
      <p>¿Estás seguro de que quieres eliminar este elemento? Esta acción no se puede deshacer.</p>
    </div>
    <div class="modal-footer">
      <button class="btn btn-secondary modal-cancel">Cancelar</button>
      <button class="btn btn-danger">Eliminar</button>
    </div>
  </div>
</div>

<!-- Modal de Formulario -->
<div class="modal-overlay" id="form-modal">
  <div class="modal-container">
    <div class="modal-header">
      <h2 class="modal-title">Formulario de Contacto</h2>
      <button class="modal-close" aria-label="Cerrar modal">
        <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>
    </div>
    <div class="modal-body">
      <form class="modal-form">
        <div class="form-group">
          <label for="modal-name">Nombre</label>
          <input type="text" id="modal-name" name="name" required>
        </div>
        <div class="form-group">
          <label for="modal-email">Email</label>
          <input type="email" id="modal-email" name="email" required>
        </div>
        <div class="form-group">
          <label for="modal-message">Mensaje</label>
          <textarea id="modal-message" name="message" rows="4" required></textarea>
        </div>
      </form>
    </div>
    <div class="modal-footer">
      <button class="btn btn-secondary modal-cancel">Cancelar</button>
      <button class="btn btn-primary" type="submit">Enviar Mensaje</button>
    </div>
  </div>
</div>

<!-- Modal de Imagen -->
<div class="modal-overlay" id="image-modal">
  <div class="modal-container modal-image">
    <button class="modal-close" aria-label="Cerrar modal">
      <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>
    <div class="image-gallery">
      <div class="gallery-main">
        <img src="https://picsum.photos/800/600?random=1" alt="Imagen de galería" class="gallery-image">
        <div class="gallery-controls">
          <button class="gallery-btn prev" aria-label="Imagen anterior">
            <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>
          </button>
          <button class="gallery-btn next" aria-label="Siguiente imagen">
            <svg viewBox="0 0 24 24" fill="currentColor">
              <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
            </svg>
          </button>
        </div>
      </div>
      <div class="gallery-info">
        <h3>Galería de Imágenes</h3>
        <p>Imagen 1 de 5</p>
      </div>
    </div>
  </div>
</div>

<!-- Modal de Video -->
<div class="modal-overlay" id="video-modal">
  <div class="modal-container modal-video">
    <button class="modal-close" aria-label="Cerrar modal">
      <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>
    <div class="video-container">
      <iframe 
        src="https://www.youtube.com/embed/dQw4w9WgXcQ" 
        frameborder="0" 
        allowfullscreen
        title="Reproductor de video">
      </iframe>
    </div>
  </div>
</div>

<!-- Modal de Pantalla Completa -->
<div class="modal-overlay" id="fullscreen-modal">
  <div class="modal-container modal-fullscreen">
    <div class="modal-header">
      <h2 class="modal-title">Modal de Pantalla Completa</h2>
      <button class="modal-close" aria-label="Cerrar modal">
        <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>
    </div>
    <div class="modal-body">
      <div class="fullscreen-content">
        <h3>Experiencia de Pantalla Completa</h3>
        <p>Este modal ocupa todo el viewport, perfecto para contenido inmersivo, formularios detallados o interfaces complejas.</p>
        <div class="content-grid">
          <div class="content-card">
            <h4>Característica 1</h4>
            <p>Descripción detallada de la primera característica con información completa.</p>
          </div>
          <div class="content-card">
            <h4>Característica 2</h4>
            <p>Descripción detallada de la segunda característica con información completa.</p>
          </div>
          <div class="content-card">
            <h4>Característica 3</h4>
            <p>Descripción detallada de la tercera característica con información completa.</p>
          </div>
        </div>
      </div>
    </div>
    <div class="modal-footer">
      <button class="btn btn-secondary modal-cancel">Cerrar</button>
      <button class="btn btn-primary">Guardar Cambios</button>
    </div>
  </div>
</div>

<!-- Popup Tooltip -->
<div class="popup-container" id="tooltip-popup">
  <div class="popup-content tooltip">
    <div class="tooltip-arrow"></div>
    <p>Este es un tooltip útil que proporciona información adicional sobre el elemento.</p>
  </div>
</div>

<!-- Popup de Notificación -->
<div class="popup-container" id="notification-popup">
  <div class="popup-content notification success">
    <div class="notification-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>
    <div class="notification-content">
      <h4>¡Éxito!</h4>
      <p>Tu acción se ha completado exitosamente.</p>
    </div>
    <button class="notification-close" aria-label="Cerrar notificación">
      <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>
  </div>
</div>

<!-- Popup Desplegable -->
<div class="popup-container" id="dropdown-popup">
  <div class="popup-content dropdown">
    <div class="dropdown-header">
      <h4>Opciones</h4>
    </div>
    <div class="dropdown-list">
      <a href="#" class="dropdown-item">
        <span class="dropdown-icon">📄</span>
        Ver Detalles
      </a>
      <a href="#" class="dropdown-item">
        <span class="dropdown-icon">✏️</span>
        Editar Elemento
      </a>
      <a href="#" class="dropdown-item">
        <span class="dropdown-icon">📋</span>
        Copiar Enlace
      </a>
      <div class="dropdown-divider"></div>
      <a href="#" class="dropdown-item danger">
        <span class="dropdown-icon">🗑️</span>
        Eliminar Elemento
      </a>
    </div>
  </div>
</div>

<!-- Popup de Menú Contextual -->
<div class="popup-container" id="context-popup">
  <div class="popup-content context-menu">
    <div class="context-list">
      <button class="context-item">
        <span class="context-icon">📋</span>
        Copiar
        <span class="context-shortcut">Ctrl+C</span>
      </button>
      <button class="context-item">
        <span class="context-icon">📄</span>
        Pegar
        <span class="context-shortcut">Ctrl+V</span>
      </button>
      <button class="context-item">
        <span class="context-icon">✂️</span>
        Cortar
        <span class="context-shortcut">Ctrl+X</span>
      </button>
      <div class="context-divider"></div>
      <button class="context-item">
        <span class="context-icon">🔄</span>
        Actualizar
        <span class="context-shortcut">F5</span>
      </button>
      <button class="context-item">
        <span class="context-icon">⚙️</span>
        Configuración
      </button>
    </div>
  </div>
</div>
.modal-demo-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.modal-triggers,
.popup-triggers {
  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;
}

.modal-triggers h3,
.popup-triggers h3 {
  margin: 0 0 2rem 0;
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a1a;
  text-align: center;
}

.trigger-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
}

.trigger-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 1.5rem;
  border: 2px solid #e5e7eb;
  background: white;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  font-weight: 600;
  color: #374151;
}

.trigger-btn:hover {
  border-color: #3b82f6;
  background: #f8fafc;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}

.trigger-icon {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

/* Superposición de Modal */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s ease;
  padding: 1rem;
}

.modal-overlay.active {
  opacity: 1;
  visibility: visible;
}

.modal-overlay.active .modal-container {
  transform: scale(1) translateY(0);
  opacity: 1;
}

/* Contenedor de Modal */
.modal-container {
  background: white;
  border-radius: 16px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  max-width: 500px;
  width: 100%;
  max-height: 90vh;
  overflow: hidden;
  transform: scale(0.9) translateY(20px);
  opacity: 0;
  transition: all 0.3s ease;
  position: relative;
}

.modal-container.modal-small {
  max-width: 400px;
}

.modal-container.modal-large {
  max-width: 800px;
}

.modal-container.modal-fullscreen {
  max-width: 95vw;
  max-height: 95vh;
  width: 95vw;
  height: 95vh;
}

.modal-container.modal-image {
  max-width: 90vw;
  max-height: 90vh;
  background: transparent;
  box-shadow: none;
}

.modal-container.modal-video {
  max-width: 80vw;
  max-height: 80vh;
  background: #000;
  border-radius: 8px;
}

/* Encabezado de Modal */
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.5rem 2rem;
  border-bottom: 1px solid #e5e7eb;
  background: #f9fafb;
}

.modal-title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 700;
  color: #1a1a1a;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.modal-icon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 1rem;
}

.modal-icon.warning {
  background: #fef3c7;
  color: #d97706;
}

.modal-icon.success {
  background: #d1fae5;
  color: #059669;
}

.modal-icon.error {
  background: #fee2e2;
  color: #dc2626;
}

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

.modal-close {
  width: 40px;
  height: 40px;
  border: none;
  background: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #6b7280;
  transition: all 0.2s ease;
}

.modal-close:hover {
  background: #f3f4f6;
  color: #374151;
}

.modal-close svg {
  width: 20px;
  height: 20px;
}

/* Cuerpo de Modal */
.modal-body {
  padding: 2rem;
  overflow-y: auto;
  max-height: 60vh;
}

.modal-body p {
  margin: 0 0 1rem 0;
  line-height: 1.6;
  color: #374151;
}

.modal-body p:last-child {
  margin-bottom: 0;
}

/* Pie de Modal */
.modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1rem;
  padding: 1.5rem 2rem;
  border-top: 1px solid #e5e7eb;
  background: #f9fafb;
}

/* Estilos de Formulario */
.modal-form {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.form-group label {
  font-weight: 600;
  color: #374151;
  font-size: 0.875rem;
}

.form-group input,
.form-group textarea {
  padding: 0.75rem;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  font-size: 1rem;
  transition: border-color 0.2s ease;
}

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

/* Estilos de Botón */
.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 0.875rem;
}

.btn-primary {
  background: #3b82f6;
  color: white;
}

.btn-primary:hover {
  background: #2563eb;
  transform: translateY(-1px);
}

.btn-secondary {
  background: #f3f4f6;
  color: #374151;
  border: 1px solid #d1d5db;
}

.btn-secondary:hover {
  background: #e5e7eb;
}

.btn-danger {
  background: #ef4444;
  color: white;
}

.btn-danger:hover {
  background: #dc2626;
  transform: translateY(-1px);
}

/* Galería de Imágenes */
.image-gallery {
  position: relative;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
}

.gallery-main {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
}

.gallery-image {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
}

.gallery-controls {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  justify-content: space-between;
  padding: 0 1rem;
  pointer-events: none;
}

.gallery-btn {
  width: 50px;
  height: 50px;
  border: none;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s ease;
  pointer-events: auto;
}

.gallery-btn:hover {
  background: white;
  transform: scale(1.1);
}

.gallery-btn svg {
  width: 24px;
  height: 24px;
  color: #374151;
}

.gallery-info {
  padding: 1rem;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  text-align: center;
}

.gallery-info h3 {
  margin: 0 0 0.5rem 0;
  font-size: 1.125rem;
}

.gallery-info p {
  margin: 0;
  color: #d1d5db;
  font-size: 0.875rem;
}

/* Contenedor de Video */
.video-container {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; /* Relación de aspecto 16:9 */
}

.video-container iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* Contenido de Pantalla Completa */
.fullscreen-content {
  padding: 2rem 0;
}

.content-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
  margin-top: 2rem;
}

.content-card {
  padding: 2rem;
  background: #f9fafb;
  border-radius: 12px;
  border: 1px solid #e5e7eb;
}

.content-card h4 {
  margin: 0 0 1rem 0;
  font-size: 1.125rem;
  font-weight: 700;
  color: #1a1a1a;
}

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

/* Contenedor de Popup */
.popup-container {
  position: fixed;
  z-index: 1100;
  opacity: 0;
  visibility: hidden;
  transition: all 0.2s ease;
  pointer-events: none;
}

.popup-container.active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.popup-content {
  background: white;
  border-radius: 8px;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  border: 1px solid #e5e7eb;
  transform: scale(0.95) translateY(-10px);
  transition: all 0.2s ease;
}

.popup-container.active .popup-content {
  transform: scale(1) translateY(0);
}

/* Tooltip */
.tooltip {
  padding: 0.75rem 1rem;
  max-width: 250px;
  font-size: 0.875rem;
  color: #374151;
  line-height: 1.4;
  position: relative;
}

.tooltip-arrow {
  position: absolute;
  bottom: -6px;
  left: 50%;
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 6px solid white;
}

/* Notificación */
.notification {
  padding: 1rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  min-width: 300px;
  max-width: 400px;
  position: relative;
}

.notification.success {
  background: #f0fdf4;
  border-left: 4px solid #22c55e;
}

.notification.warning {
  background: #fffbeb;
  border-left: 4px solid #f59e0b;
}

.notification.error {
  background: #fef2f2;
  border-left: 4px solid #ef4444;
}

.notification-icon {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
}

.notification.success .notification-icon {
  color: #22c55e;
}

.notification.warning .notification-icon {
  color: #f59e0b;
}

.notification.error .notification-icon {
  color: #ef4444;
}

.notification-content h4 {
  margin: 0 0 0.25rem 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: #1a1a1a;
}

.notification-content p {
  margin: 0;
  font-size: 0.875rem;
  color: #6b7280;
}

.notification-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 24px;
  height: 24px;
  border: none;
  background: none;
  cursor: pointer;
  color: #9ca3af;
  transition: color 0.2s ease;
}

.notification-close:hover {
  color: #6b7280;
}

.notification-close svg {
  width: 16px;
  height: 16px;
}

/* Desplegable */
.dropdown {
  min-width: 200px;
  padding: 0.5rem 0;
}

.dropdown-header {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid #e5e7eb;
  margin-bottom: 0.5rem;
}

.dropdown-header h4 {
  margin: 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: #374151;
}

.dropdown-list {
  display: flex;
  flex-direction: column;
}

.dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  text-decoration: none;
  color: #374151;
  font-size: 0.875rem;
  transition: background-color 0.2s ease;
}

.dropdown-item:hover {
  background: #f3f4f6;
}

.dropdown-item.danger {
  color: #dc2626;
}

.dropdown-item.danger:hover {
  background: #fef2f2;
}

.dropdown-icon {
  font-size: 1rem;
}

.dropdown-divider {
  height: 1px;
  background: #e5e7eb;
  margin: 0.5rem 0;
}

/* Menú Contextual */
.context-menu {
  min-width: 180px;
  padding: 0.5rem 0;
}

.context-list {
  display: flex;
  flex-direction: column;
}

.context-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 1rem;
  border: none;
  background: none;
  text-align: left;
  cursor: pointer;
  font-size: 0.875rem;
  color: #374151;
  transition: background-color 0.2s ease;
}

.context-item:hover {
  background: #f3f4f6;
}

.context-icon {
  margin-right: 0.75rem;
  font-size: 1rem;
}

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

.context-divider {
  height: 1px;
  background: #e5e7eb;
  margin: 0.5rem 0;
}

/* Tema Oscuro */
.dark .modal-container {
  background: #1f2937;
  color: #e5e7eb;
}

.dark .modal-header,
.dark .modal-footer {
  background: #111827;
  border-color: #374151;
}

.dark .modal-title {
  color: #f9fafb;
}

.dark .modal-body p {
  color: #d1d5db;
}

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

.dark .form-group input:focus,
.dark .form-group textarea:focus {
  border-color: #3b82f6;
}

.dark .content-card {
  background: #111827;
  border-color: #374151;
}

.dark .popup-content {
  background: #1f2937;
  border-color: #374151;
  color: #e5e7eb;
}

.dark .dropdown-item,
.dark .context-item {
  color: #d1d5db;
}

.dark .dropdown-item:hover,
.dark .context-item:hover {
  background: #374151;
}

/* Diseño Responsivo */
@media (max-width: 768px) {
  .modal-demo-container {
    padding: 1rem;
  }
  
  .modal-triggers,
  .popup-triggers {
    padding: 1.5rem;
  }
  
  .trigger-grid {
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  }
  
  .modal-overlay {
    padding: 0.5rem;
  }
  
  .modal-container {
    max-width: 100%;
    margin: 0;
  }
  
  .modal-container.modal-fullscreen {
    max-width: 100vw;
    max-height: 100vh;
    width: 100vw;
    height: 100vh;
    border-radius: 0;
  }
  
  .modal-header,
  .modal-footer {
    padding: 1rem 1.5rem;
  }
  
  .modal-body {
    padding: 1.5rem;
  }
  
  .content-grid {
    grid-template-columns: 1fr;
  }
  
  .notification {
    min-width: 280px;
    max-width: 320px;
  }
}

@media (max-width: 480px) {
  .trigger-grid {
    grid-template-columns: 1fr;
  }
  
  .trigger-btn {
    padding: 1rem;
  }
  
  .modal-header,
  .modal-footer {
    padding: 1rem;
  }
  
  .modal-body {
    padding: 1rem;
  }
  
  .modal-footer {
    flex-direction: column;
    gap: 0.5rem;
  }
  
  .btn {
    width: 100%;
  }
  
  .gallery-controls {
    padding: 0 0.5rem;
  }
  
  .gallery-btn {
    width: 40px;
    height: 40px;
  }
}

/* Movimiento Reducido */
@media (prefers-reduced-motion: reduce) {
  .modal-overlay,
  .modal-container,
  .popup-container,
  .popup-content {
    transition: none;
  }
  
  .modal-overlay.active .modal-container {
    transform: none;
  }
  
  .popup-container.active .popup-content {
    transform: none;
  }
}

/* Estilos de Foco */
.modal-close:focus,
.btn:focus,
.dropdown-item:focus,
.context-item:focus {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

/* Clases de Animación */
.fade-in {
  animation: fadeIn 0.3s ease;
}

.slide-up {
  animation: slideUp 0.3s ease;
}

.scale-in {
  animation: scaleIn 0.3s ease;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

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

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
class ComponentesModalesPopups {
  constructor() {
    this.modales = new Map();
    this.popups = new Map();
    this.modalActivo = null;
    this.popupActivo = null;
    this.pilaModales = [];
    this.elementosEnfocables = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
    
    this.init();
  }

  init() {
    this.configurarEventListeners();
    this.inicializarModales();
    this.inicializarPopups();
    this.configurarNavegacionTeclado();
    this.configurarAccesibilidad();
  }

  configurarEventListeners() {
    // Activadores de modal
    document.addEventListener('click', (e) => {
      const activador = e.target.closest('[data-modal]');
      if (activador) {
        e.preventDefault();
        const modalId = activador.dataset.modal;
        this.abrirModal(modalId);
      }
      
      // Activadores de popup
      const activadorPopup = e.target.closest('[data-popup]');
      if (activadorPopup) {
        e.preventDefault();
        const popupId = activadorPopup.dataset.popup;
        this.abrirPopup(popupId, activadorPopup);
      }
      
      // Botones de cerrar
      if (e.target.closest('.modal-close, .modal-cancel')) {
        e.preventDefault();
        this.cerrarModalActivo();
      }
      
      if (e.target.closest('.notification-close')) {
        e.preventDefault();
        this.cerrarPopupActivo();
      }
      
      // Clic en fondo
      if (e.target.classList.contains('modal-overlay')) {
        this.cerrarModalActivo();
      }
    });

    // Menú contextual
    document.addEventListener('contextmenu', (e) => {
      const activadorContexto = e.target.closest('[data-context]');
      if (activadorContexto) {
        e.preventDefault();
        this.abrirMenuContextual(e, 'context-popup');
      }
    });

    // Cerrar popups al hacer clic fuera
    document.addEventListener('click', (e) => {
      if (this.popupActivo && !e.target.closest('.popup-container')) {
        this.cerrarPopupActivo();
      }
    });
  }

  inicializarModales() {
    const elementosModal = document.querySelectorAll('.modal-overlay');
    elementosModal.forEach(modal => {
      const modalId = modal.id;
      this.modales.set(modalId, {
        elemento: modal,
        contenedor: modal.querySelector('.modal-container'),
        estaAbierto: false,
        focoAnterior: null
      });
    });
  }

  inicializarPopups() {
    const elementosPopup = document.querySelectorAll('.popup-container');
    elementosPopup.forEach(popup => {
      const popupId = popup.id;
      this.popups.set(popupId, {
        elemento: popup,
        contenido: popup.querySelector('.popup-content'),
        estaAbierto: false,
        activador: null
      });
    });
  }

  configurarNavegacionTeclado() {
    document.addEventListener('keydown', (e) => {
      // Tecla Escape
      if (e.key === 'Escape') {
        if (this.popupActivo) {
          this.cerrarPopupActivo();
        } else if (this.modalActivo) {
          this.cerrarModalActivo();
        }
      }
      
      // Navegación Tab dentro del modal
      if (e.key === 'Tab' && this.modalActivo) {
        this.manejarNavegacionTab(e);
      }
    });
  }

  configurarAccesibilidad() {
    // Agregar atributos ARIA
    this.modales.forEach((modal, id) => {
      modal.elemento.setAttribute('role', 'dialog');
      modal.elemento.setAttribute('aria-modal', 'true');
      modal.elemento.setAttribute('aria-hidden', 'true');
      
      const titulo = modal.elemento.querySelector('.modal-title');
      if (titulo) {
        titulo.id = `${id}-title`;
        modal.elemento.setAttribute('aria-labelledby', `${id}-title`);
      }
    });

    this.popups.forEach((popup, id) => {
      popup.elemento.setAttribute('role', 'tooltip');
      popup.elemento.setAttribute('aria-hidden', 'true');
    });
  }

  abrirModal(modalId, opciones = {}) {
    const modal = this.modales.get(modalId);
    if (!modal || modal.estaAbierto) return;

    // Guardar foco anterior
    modal.focoAnterior = document.activeElement;

    // Cerrar cualquier popup activo
    if (this.popupActivo) {
      this.cerrarPopupActivo();
    }

    // Agregar a la pila de modales
    this.pilaModales.push(modalId);
    this.modalActivo = modalId;

    // Mostrar modal
    modal.elemento.style.display = 'flex';
    modal.elemento.setAttribute('aria-hidden', 'false');
    
    // Activar animación
    requestAnimationFrame(() => {
      modal.elemento.classList.add('active');
      if (opciones.animacion) {
        modal.contenedor.classList.add(opciones.animacion);
      }
    });

    // Gestión de foco
    setTimeout(() => {
      this.enfocarPrimerElemento(modal.elemento);
    }, 100);

    // Prevenir scroll del body
    document.body.style.overflow = 'hidden';
    modal.estaAbierto = true;

    // Disparar evento
    this.dispararEvento('modalAbierto', { modalId, modal: modal.elemento });
  }

  cerrarModal(modalId) {
    const modal = this.modales.get(modalId);
    if (!modal || !modal.estaAbierto) return;

    // Remover de la pila
    const indiceEnPila = this.pilaModales.indexOf(modalId);
    if (indiceEnPila > -1) {
      this.pilaModales.splice(indiceEnPila, 1);
    }

    // Actualizar modal activo
    this.modalActivo = this.pilaModales.length > 0 ? this.pilaModales[this.pilaModales.length - 1] : null;

    // Ocultar modal
    modal.elemento.classList.remove('active');
    modal.elemento.setAttribute('aria-hidden', 'true');

    setTimeout(() => {
      modal.elemento.style.display = 'none';
      modal.contenedor.className = modal.contenedor.className.replace(/\b(fade-in|slide-up|scale-in)\b/g, '');
    }, 300);

    // Restaurar foco
    if (modal.focoAnterior) {
      modal.focoAnterior.focus();
      modal.focoAnterior = null;
    }

    // Restaurar scroll del body si no hay modales abiertos
    if (this.pilaModales.length === 0) {
      document.body.style.overflow = '';
    }

    modal.estaAbierto = false;

    // Disparar evento
    this.dispararEvento('modalCerrado', { modalId, modal: modal.elemento });
  }

  cerrarModalActivo() {
    if (this.modalActivo) {
      this.cerrarModal(this.modalActivo);
    }
  }

  abrirPopup(popupId, activador, opciones = {}) {
    const popup = this.popups.get(popupId);
    if (!popup || popup.estaAbierto) return;

    // Cerrar cualquier popup activo
    if (this.popupActivo) {
      this.cerrarPopupActivo();
    }

    this.popupActivo = popupId;
    popup.activador = activador;

    // Posicionar popup
    this.posicionarPopup(popup, activador, opciones.posicion);

    // Mostrar popup
    popup.elemento.style.display = 'block';
    popup.elemento.setAttribute('aria-hidden', 'false');
    
    requestAnimationFrame(() => {
      popup.elemento.classList.add('active');
    });

    popup.estaAbierto = true;

    // Auto-cerrar para notificaciones
    if (popup.elemento.querySelector('.notification') && opciones.autoCerrar !== false) {
      setTimeout(() => {
        this.cerrarPopup(popupId);
      }, opciones.duracion || 5000);
    }

    // Disparar evento
    this.dispararEvento('popupAbierto', { popupId, popup: popup.elemento, activador });
  }

  cerrarPopup(popupId) {
    const popup = this.popups.get(popupId);
    if (!popup || !popup.estaAbierto) return;

    popup.elemento.classList.remove('active');
    popup.elemento.setAttribute('aria-hidden', 'true');

    setTimeout(() => {
      popup.elemento.style.display = 'none';
    }, 200);

    if (this.popupActivo === popupId) {
      this.popupActivo = null;
    }

    popup.estaAbierto = false;
    popup.activador = null;

    // Disparar evento
    this.dispararEvento('popupCerrado', { popupId, popup: popup.elemento });
  }

  cerrarPopupActivo() {
    if (this.popupActivo) {
      this.cerrarPopup(this.popupActivo);
    }
  }

  abrirMenuContextual(evento, popupId) {
    const popup = this.popups.get(popupId);
    if (!popup) return;

    // Cerrar cualquier popup activo
    if (this.popupActivo) {
      this.cerrarPopupActivo();
    }

    this.popupActivo = popupId;

    // Posicionar en el cursor
    popup.elemento.style.position = 'fixed';
    popup.elemento.style.left = `${evento.clientX}px`;
    popup.elemento.style.top = `${evento.clientY}px`;

    // Ajustar posición si está cerca de los bordes del viewport
    const rect = popup.contenido.getBoundingClientRect();
    const anchoViewport = window.innerWidth;
    const altoViewport = window.innerHeight;

    if (evento.clientX + rect.width > anchoViewport) {
      popup.elemento.style.left = `${evento.clientX - rect.width}px`;
    }

    if (evento.clientY + rect.height > altoViewport) {
      popup.elemento.style.top = `${evento.clientY - rect.height}px`;
    }

    // Mostrar popup
    popup.elemento.style.display = 'block';
    popup.elemento.setAttribute('aria-hidden', 'false');
    
    requestAnimationFrame(() => {
      popup.elemento.classList.add('active');
    });

    popup.estaAbierto = true;
  }

  posicionarPopup(popup, activador, posicion = 'bottom') {
    const rectActivador = activador.getBoundingClientRect();
    const rectPopup = popup.contenido.getBoundingClientRect();
    const anchoViewport = window.innerWidth;
    const altoViewport = window.innerHeight;
    const scrollX = window.pageXOffset;
    const scrollY = window.pageYOffset;

    let izquierda, arriba;

    switch (posicion) {
      case 'top':
        izquierda = rectActivador.left + (rectActivador.width / 2) - (rectPopup.width / 2);
        arriba = rectActivador.top - rectPopup.height - 10;
        break;
      case 'bottom':
        izquierda = rectActivador.left + (rectActivador.width / 2) - (rectPopup.width / 2);
        arriba = rectActivador.bottom + 10;
        break;
      case 'left':
        izquierda = rectActivador.left - rectPopup.width - 10;
        arriba = rectActivador.top + (rectActivador.height / 2) - (rectPopup.height / 2);
        break;
      case 'right':
        izquierda = rectActivador.right + 10;
        arriba = rectActivador.top + (rectActivador.height / 2) - (rectPopup.height / 2);
        break;
      default:
        izquierda = rectActivador.left;
        arriba = rectActivador.bottom + 10;
    }

    // Ajustar para límites del viewport
    if (izquierda < 0) izquierda = 10;
    if (izquierda + rectPopup.width > anchoViewport) izquierda = anchoViewport - rectPopup.width - 10;
    if (arriba < 0) arriba = 10;
    if (arriba + rectPopup.height > altoViewport) arriba = altoViewport - rectPopup.height - 10;

    popup.elemento.style.position = 'fixed';
    popup.elemento.style.left = `${izquierda}px`;
    popup.elemento.style.top = `${arriba}px`;
  }

  manejarNavegacionTab(evento) {
    const modal = this.modales.get(this.modalActivo);
    if (!modal) return;

    const elementosEnfocables = modal.elemento.querySelectorAll(this.elementosEnfocables);
    const primerElemento = elementosEnfocables[0];
    const ultimoElemento = elementosEnfocables[elementosEnfocables.length - 1];

    if (evento.shiftKey) {
      if (document.activeElement === primerElemento) {
        evento.preventDefault();
        ultimoElemento.focus();
      }
    } else {
      if (document.activeElement === ultimoElemento) {
        evento.preventDefault();
        primerElemento.focus();
      }
    }
  }

  enfocarPrimerElemento(contenedor) {
    const elementosEnfocables = contenedor.querySelectorAll(this.elementosEnfocables);
    if (elementosEnfocables.length > 0) {
      elementosEnfocables[0].focus();
    }
  }

  dispararEvento(nombreEvento, detalle) {
    const evento = new CustomEvent(nombreEvento, {
      detail: detalle,
      bubbles: true,
      cancelable: true
    });
    document.dispatchEvent(evento);
  }

  // API Pública
  mostrar(id, opciones = {}) {
    if (this.modales.has(id)) {
      this.abrirModal(id, opciones);
    } else if (this.popups.has(id)) {
      // Para popups, necesitamos un elemento activador
      const activador = opciones.activador || document.body;
      this.abrirPopup(id, activador, opciones);
    }
  }

  ocultar(id) {
    if (this.modales.has(id)) {
      this.cerrarModal(id);
    } else if (this.popups.has(id)) {
      this.cerrarPopup(id);
    }
  }

  ocultarTodos() {
    // Cerrar todos los modales
    this.pilaModales.forEach(modalId => {
      this.cerrarModal(modalId);
    });
    
    // Cerrar popup activo
    if (this.popupActivo) {
      this.cerrarPopupActivo();
    }
  }

  estaAbierto(id) {
    const modal = this.modales.get(id);
    const popup = this.popups.get(id);
    return (modal && modal.estaAbierto) || (popup && popup.estaAbierto);
  }

  mostrarNotificacion(mensaje, tipo = 'success', opciones = {}) {
    const notificacion = this.crearNotificacion(mensaje, tipo, opciones);
    document.body.appendChild(notificacion);
    
    // Posicionar notificación
    this.posicionarNotificacion(notificacion, opciones.posicion || 'top-right');
    
    // Mostrar con animación
    requestAnimationFrame(() => {
      notificacion.classList.add('active');
    });
    
    // Auto-cerrar
    if (opciones.autoCerrar !== false) {
      setTimeout(() => {
        this.cerrarNotificacion(notificacion);
      }, opciones.duracion || 5000);
    }
    
    return notificacion;
  }

  crearNotificacion(mensaje, tipo, opciones) {
    const notificacion = document.createElement('div');
    notificacion.className = 'popup-container notification-popup';
    notificacion.innerHTML = `
      <div class="popup-content notification ${tipo}">
        <div class="notification-icon">
          ${this.obtenerIconoNotificacion(tipo)}
        </div>
        <div class="notification-content">
          <h4>${opciones.titulo || this.obtenerTituloNotificacion(tipo)}</h4>
          <p>${mensaje}</p>
        </div>
        <button class="notification-close" aria-label="Cerrar notificación">
          <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>
      </div>
    `;
    
    // Agregar evento de cerrar
    notificacion.querySelector('.notification-close').addEventListener('click', () => {
      this.cerrarNotificacion(notificacion);
    });
    
    return notificacion;
  }

  obtenerIconoNotificacion(tipo) {
    const iconos = {
      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>',
      warning: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>',
      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>',
      info: '<svg 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 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
    };
    return iconos[tipo] || iconos.info;
  }

  obtenerTituloNotificacion(tipo) {
    const titulos = {
      success: '¡Éxito!',
      warning: 'Advertencia',
      error: 'Error',
      info: 'Información'
    };
    return titulos[tipo] || 'Notificación';
  }

  posicionarNotificacion(notificacion, posicion) {
    const posiciones = {
      'top-right': { top: '20px', right: '20px' },
      'top-left': { top: '20px', left: '20px' },
      'bottom-right': { bottom: '20px', right: '20px' },
      'bottom-left': { bottom: '20px', left: '20px' },
      'top-center': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
      'bottom-center': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' }
    };
    
    const estilos = posiciones[posicion] || posiciones['top-right'];
    Object.assign(notificacion.style, {
      position: 'fixed',
      zIndex: '1200',
      ...estilos
    });
  }

  cerrarNotificacion(notificacion) {
    notificacion.classList.remove('active');
    setTimeout(() => {
      if (notificacion.parentNode) {
        notificacion.parentNode.removeChild(notificacion);
      }
    }, 300);
  }

  destruir() {
    // Cerrar todos los modales y popups
    this.ocultarTodos();
    
    // Limpiar event listeners
    document.removeEventListener('click', this.configurarEventListeners);
    document.removeEventListener('keydown', this.configurarNavegacionTeclado);
    
    // Restaurar scroll del body
    document.body.style.overflow = '';
    
    // Limpiar mapas
    this.modales.clear();
    this.popups.clear();
    this.pilaModales = [];
    this.modalActivo = null;
    this.popupActivo = null;
  }
}

// Inicialización automática
let componentesModalesPopups;
document.addEventListener('DOMContentLoaded', () => {
  componentesModalesPopups = new ComponentesModalesPopups();
});

// Exportar para uso como módulo
if (typeof module !== 'undefined' && module.exports) {
  module.exports = ComponentesModalesPopups;
}

// API global
window.ComponentesModalesPopups = ComponentesModalesPopups;

Ejemplos de Uso

// Abrir modal programáticamente
componentesModalesPopups.mostrar('basic-modal', {
  animacion: 'fade-in'
});

// Cerrar modal
componentesModalesPopups.ocultar('basic-modal');

// Verificar si está abierto
if (componentesModalesPopups.estaAbierto('basic-modal')) {
  console.log('El modal está abierto');
}

Sistema de Notificaciones

// Mostrar notificación de éxito
componentesModalesPopups.mostrarNotificacion(
  'Los datos se han guardado correctamente',
  'success',
  {
    titulo: 'Guardado',
    posicion: 'top-right',
    duracion: 3000
  }
);

// Mostrar notificación de error
componentesModalesPopups.mostrarNotificacion(
  'Ha ocurrido un error al procesar la solicitud',
  'error',
  {
    autoCerrar: false // No cerrar automáticamente
  }
);

Manejo de Eventos

// Escuchar eventos de modal
document.addEventListener('modalAbierto', (e) => {
  console.log('Modal abierto:', e.detail.modalId);
});

document.addEventListener('modalCerrado', (e) => {
  console.log('Modal cerrado:', e.detail.modalId);
});

// Escuchar eventos de popup
document.addEventListener('popupAbierto', (e) => {
  console.log('Popup abierto:', e.detail.popupId);
});

Creación Dinámica de Modales

// Crear modal personalizado
function crearModalPersonalizado(titulo, contenido) {
  const modalHtml = `
    <div class="modal-overlay" id="modal-personalizado">
      <div class="modal-container">
        <div class="modal-header">
          <h2 class="modal-title">${titulo}</h2>
          <button class="modal-close" aria-label="Cerrar modal">
            <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>
        </div>
        <div class="modal-body">
          ${contenido}
        </div>
        <div class="modal-footer">
          <button class="btn btn-secondary modal-cancel">Cancelar</button>
          <button class="btn btn-primary">Aceptar</button>
        </div>
      </div>
    </div>
  `;
  
  document.body.insertAdjacentHTML('beforeend', modalHtml);
  componentesModalesPopups.inicializarModales();
  componentesModalesPopups.mostrar('modal-personalizado');
}

Referencia de API

Métodos

MétodoDescripciónParámetros
mostrar(id, opciones)Abre un modal o popupid: ID del elemento, opciones: objeto de configuración
ocultar(id)Cierra un modal o popup específicoid: ID del elemento
ocultarTodos()Cierra todos los modales y popups abiertos-
estaAbierto(id)Verifica si un modal/popup está abiertoid: ID del elemento
mostrarNotificacion(mensaje, tipo, opciones)Muestra una notificaciónmensaje: texto, tipo: success/warning/error/info, opciones: configuración
destruir()Limpia todos los event listeners y elementos-

Eventos

EventoDescripciónDetalle
modalAbiertoSe dispara cuando se abre un modal{ modalId, modal }
modalCerradoSe dispara cuando se cierra un modal{ modalId, modal }
popupAbiertoSe dispara cuando se abre un popup{ popupId, popup, activador }
popupCerradoSe dispara cuando se cierra un popup{ popupId, popup }

Opciones

Opciones de Modal

{
  animacion: 'fade-in' | 'slide-up' | 'scale-in', // Tipo de animación
  cerrarAlHacerClicFuera: true, // Cerrar al hacer clic en el fondo
  cerrarConEscape: true // Cerrar con la tecla Escape
}

Opciones de Popup

{
  posicion: 'top' | 'bottom' | 'left' | 'right', // Posición relativa al activador
  activador: Element, // Elemento que activa el popup
  autoCerrar: true, // Cerrar automáticamente
  duracion: 5000 // Duración antes del auto-cierre (ms)
}

Opciones de Notificación

{
  titulo: 'Título personalizado', // Título de la notificación
  posicion: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center',
  autoCerrar: true, // Cerrar automáticamente
  duracion: 5000 // Duración antes del auto-cierre (ms)
}

Personalización

Variables CSS

:root {
  --modal-overlay-bg: rgba(0, 0, 0, 0.5);
  --modal-bg: white;
  --modal-border-radius: 16px;
  --modal-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
  --modal-header-bg: #f9fafb;
  --modal-footer-bg: #f9fafb;
  --popup-bg: white;
  --popup-border-radius: 8px;
  --popup-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
  --notification-success-bg: #f0fdf4;
  --notification-warning-bg: #fffbeb;
  --notification-error-bg: #fef2f2;
  --animation-duration: 0.3s;
  --animation-timing: ease;
}

Animaciones Personalizadas

/* Animación personalizada */
@keyframes slideInFromRight {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

.modal-container.slide-right {
  animation: slideInFromRight 0.3s ease;
}

Accesibilidad

  • Navegación por Teclado: Soporte completo para Tab, Shift+Tab y Escape
  • Gestión de Foco: El foco se mueve automáticamente al modal y se restaura al cerrarlo
  • Atributos ARIA: Roles y propiedades ARIA apropiados para lectores de pantalla
  • Contraste de Color: Cumple con las pautas WCAG 2.1 AA
  • Texto Alternativo: Todos los iconos incluyen etiquetas aria-label

Soporte de Navegadores

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

Consideraciones de Rendimiento

  • Lazy Loading: Los modales se inicializan solo cuando se necesitan
  • Event Delegation: Uso eficiente de event listeners
  • CSS Transforms: Animaciones optimizadas con GPU
  • Memory Management: Limpieza automática de elementos dinámicos
  • Debouncing: Prevención de múltiples activaciones rápidas

Integración

React

import { useEffect, useRef } from 'react';

function ModalComponent({ isOpen, onClose, children }) {
  const modalRef = useRef();
  
  useEffect(() => {
    if (isOpen) {
      componentesModalesPopups.mostrar('react-modal');
    } else {
      componentesModalesPopups.ocultar('react-modal');
    }
  }, [isOpen]);
  
  return (
    <div className="modal-overlay" id="react-modal" ref={modalRef}>
      <div className="modal-container">
        {children}
      </div>
    </div>
  );
}

Vue

<template>
  <div class="modal-overlay" :id="modalId" v-show="isOpen">
    <div class="modal-container">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: ['isOpen', 'modalId'],
  watch: {
    isOpen(newVal) {
      if (newVal) {
        this.$nextTick(() => {
          componentesModalesPopups.mostrar(this.modalId);
        });
      } else {
        componentesModalesPopups.ocultar(this.modalId);
      }
    }
  }
};
</script>

HTML

24

líneas

CSS

160

líneas


                <div class="modal-demo">
  <button class="open-modal-btn" onclick="openModal('modal1')">Abrir Modal</button>
  
  <div id="modal1" class="modal-overlay">
    <div class="modal-content">
      <div class="modal-header">
        <h3>Título del Modal</h3>
        <button class="close-btn" onclick="closeModal('modal1')">&times;</button>
      </div>
      <div class="modal-body">
        <p>Este es el contenido del modal. Puedes agregar cualquier contenido aquí.</p>
        <form class="modal-form">
          <input type="text" placeholder="Nombre" required>
          <input type="email" placeholder="Email" required>
          <textarea placeholder="Mensaje"></textarea>
        </form>
      </div>
      <div class="modal-footer">
        <button class="btn-secondary" onclick="closeModal('modal1')">Cancelar</button>
        <button class="btn-primary">Guardar</button>
      </div>
    </div>
  </div>
</div>

              
24líneas
917caracteres
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 →