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
Diseño Responsivo
Sí
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.
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">
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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;
}.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;
}.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;
}.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;
}.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;
}.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
padding: 1.5rem 2rem;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
}.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);
}.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);
}.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;
}.video-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}.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;
}.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 {
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;
}.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;
}.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;
}.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;
}.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;
}@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;
}
}@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;
}
}.modal-close:focus,
.btn:focus,
.dropdown-item:focus,
.context-item:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}.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() {
document.addEventListener('click', (e) => {
const activador = e.target.closest('[data-modal]');
if (activador) {
e.preventDefault();
const modalId = activador.dataset.modal;
this.abrirModal(modalId);
}
const activadorPopup = e.target.closest('[data-popup]');
if (activadorPopup) {
e.preventDefault();
const popupId = activadorPopup.dataset.popup;
this.abrirPopup(popupId, activadorPopup);
}
if (e.target.closest('.modal-close, .modal-cancel')) {
e.preventDefault();
this.cerrarModalActivo();
}
if (e.target.closest('.notification-close')) {
e.preventDefault();
this.cerrarPopupActivo();
}
if (e.target.classList.contains('modal-overlay')) {
this.cerrarModalActivo();
}
});
document.addEventListener('contextmenu', (e) => {
const activadorContexto = e.target.closest('[data-context]');
if (activadorContexto) {
e.preventDefault();
this.abrirMenuContextual(e, 'context-popup');
}
});
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) => {
if (e.key === 'Escape') {
if (this.popupActivo) {
this.cerrarPopupActivo();
} else if (this.modalActivo) {
this.cerrarModalActivo();
}
}
if (e.key === 'Tab' && this.modalActivo) {
this.manejarNavegacionTab(e);
}
});
}
configurarAccesibilidad() {
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;
modal.focoAnterior = document.activeElement;
if (this.popupActivo) {
this.cerrarPopupActivo();
}
this.pilaModales.push(modalId);
this.modalActivo = modalId;
modal.elemento.style.display = 'flex';
modal.elemento.setAttribute('aria-hidden', 'false');
requestAnimationFrame(() => {
modal.elemento.classList.add('active');
if (opciones.animacion) {
modal.contenedor.classList.add(opciones.animacion);
}
});
setTimeout(() => {
this.enfocarPrimerElemento(modal.elemento);
}, 100);
document.body.style.overflow = 'hidden';
modal.estaAbierto = true;
this.dispararEvento('modalAbierto', { modalId, modal: modal.elemento });
}
cerrarModal(modalId) {
const modal = this.modales.get(modalId);
if (!modal || !modal.estaAbierto) return;
const indiceEnPila = this.pilaModales.indexOf(modalId);
if (indiceEnPila > -1) {
this.pilaModales.splice(indiceEnPila, 1);
}
this.modalActivo = this.pilaModales.length > 0 ? this.pilaModales[this.pilaModales.length - 1] : null;
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);
if (modal.focoAnterior) {
modal.focoAnterior.focus();
modal.focoAnterior = null;
}
if (this.pilaModales.length === 0) {
document.body.style.overflow = '';
}
modal.estaAbierto = false;
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;
if (this.popupActivo) {
this.cerrarPopupActivo();
}
this.popupActivo = popupId;
popup.activador = activador;
this.posicionarPopup(popup, activador, opciones.posicion);
popup.elemento.style.display = 'block';
popup.elemento.setAttribute('aria-hidden', 'false');
requestAnimationFrame(() => {
popup.elemento.classList.add('active');
});
popup.estaAbierto = true;
if (popup.elemento.querySelector('.notification') && opciones.autoCerrar !== false) {
setTimeout(() => {
this.cerrarPopup(popupId);
}, opciones.duracion || 5000);
}
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;
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;
if (this.popupActivo) {
this.cerrarPopupActivo();
}
this.popupActivo = popupId;
popup.elemento.style.position = 'fixed';
popup.elemento.style.left = `${evento.clientX}px`;
popup.elemento.style.top = `${evento.clientY}px`;
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`;
}
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;
}
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);
}
mostrar(id, opciones = {}) {
if (this.modales.has(id)) {
this.abrirModal(id, opciones);
} else if (this.popups.has(id)) {
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() {
this.pilaModales.forEach(modalId => {
this.cerrarModal(modalId);
});
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);
this.posicionarNotificacion(notificacion, opciones.posicion || 'top-right');
requestAnimationFrame(() => {
notificacion.classList.add('active');
});
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>
`;
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() {
this.ocultarTodos();
document.removeEventListener('click', this.configurarEventListeners);
document.removeEventListener('keydown', this.configurarNavegacionTeclado);
document.body.style.overflow = '';
this.modales.clear();
this.popups.clear();
this.pilaModales = [];
this.modalActivo = null;
this.popupActivo = null;
}
}
let componentesModalesPopups;
document.addEventListener('DOMContentLoaded', () => {
componentesModalesPopups = new ComponentesModalesPopups();
});
if (typeof module !== 'undefined' && module.exports) {
module.exports = ComponentesModalesPopups;
}
window.ComponentesModalesPopups = ComponentesModalesPopups;
Ejemplos de Uso
Modal Básico
componentesModalesPopups.mostrar('basic-modal', {
animacion: 'fade-in'
});
componentesModalesPopups.ocultar('basic-modal');
if (componentesModalesPopups.estaAbierto('basic-modal')) {
console.log('El modal está abierto');
}
Sistema de Notificaciones
componentesModalesPopups.mostrarNotificacion(
'Los datos se han guardado correctamente',
'success',
{
titulo: 'Guardado',
posicion: 'top-right',
duracion: 3000
}
);
componentesModalesPopups.mostrarNotificacion(
'Ha ocurrido un error al procesar la solicitud',
'error',
{
autoCerrar: false // No cerrar automáticamente
}
);
Manejo de Eventos
document.addEventListener('modalAbierto', (e) => {
console.log('Modal abierto:', e.detail.modalId);
});
document.addEventListener('modalCerrado', (e) => {
console.log('Modal cerrado:', e.detail.modalId);
});
document.addEventListener('popupAbierto', (e) => {
console.log('Popup abierto:', e.detail.popupId);
});
Creación Dinámica de Modales
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étodo | Descripción | Parámetros |
|---|---|---|
mostrar(id, opciones) | Abre un modal o popup | id: ID del elemento, opciones: objeto de configuración |
ocultar(id) | Cierra un modal o popup específico | id: ID del elemento |
ocultarTodos() | Cierra todos los modales y popups abiertos | - |
estaAbierto(id) | Verifica si un modal/popup está abierto | id: ID del elemento |
mostrarNotificacion(mensaje, tipo, opciones) | Muestra una notificación | mensaje: texto, tipo: success/warning/error/info, opciones: configuración |
destruir() | Limpia todos los event listeners y elementos | - |
Eventos
| Evento | Descripción | Detalle |
|---|---|---|
modalAbierto | Se dispara cuando se abre un modal | { modalId, modal } |
modalCerrado | Se dispara cuando se cierra un modal | { modalId, modal } |
popupAbierto | Se dispara cuando se abre un popup | { popupId, popup, activador } |
popupCerrado | Se 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
@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
x
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')">×</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>