Implementación de sliders de comparación antes/después - Diseño y optimización de UI
Casos de uso y requisitos de diseño del slider de comparación de imágenes
Los sliders de comparación de imágenes (Before/After Sliders) superponen dos imágenes, permitiendo a los usuarios arrastrar un divisor para inspeccionar visualmente las diferencias. Las aplicaciones incluyen antes/después de edición fotográfica, comparación de calidad de compresión, iteraciones de diseño web y cambios temporales en imágenes médicas.
Requisitos de diseño:
- Interacción intuitiva: Los usuarios deben entender la operación sin instrucciones. El control deslizante debe comunicar visualmente que es arrastrable
- Diseño responsivo: Soportar tanto interacciones de ratón en escritorio como táctiles en móvil. El componente se escala apropiadamente con el ancho del viewport
- Rendimiento: Mantener 60fps durante el arrastre. Minimizar repintados y aprovechar la aceleración GPU
- Accesibilidad: Soportar operación por teclado (teclas de flecha) y comunicar el estado a lectores de pantalla mediante atributos ARIA
- Sincronización de imágenes: Ambas imágenes deben alinearse perfectamente en posición y tamaño. Definir el manejo para relaciones de aspecto no coincidentes
Existen tres enfoques principales de implementación: CSS clip-path, overflow: hidden con control de ancho, y renderizado Canvas. Cada uno tiene compensaciones - este artículo se centra en el enfoque clip-path por sus características superiores de rendimiento.
Estructura HTML y semántica - Marcado accesible
La estructura HTML debe considerar la semántica y accesibilidad, asegurando que los usuarios de lectores de pantalla comprendan la intención de comparación y puedan operar mediante teclado.
Estructura HTML recomendada:
<div class="comparison-slider" role="group" aria-label="Comparación de imágenes"><div class="comparison-slider__before"><img src="before.webp" alt="Antes del procesamiento" /><span class="comparison-slider__label">Antes</span></div><div class="comparison-slider__after"><img src="after.webp" alt="Después del procesamiento" /><span class="comparison-slider__label">Después</span></div><div class="comparison-slider__handle" role="slider" aria-label="Posición de comparación" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" tabindex="0"></div></div>
Consideraciones del marcado:
- role="group": Establecido en el contenedor para indicar elementos relacionados
- role="slider": Establecido en el control para comunicar el control deslizante a los lectores de pantalla
- aria-valuemin/max/now: Comunica la posición actual como porcentaje, actualizado dinámicamente vía JavaScript
- tabindex="0": Hace el control enfocable para operación por teclado
- Atributos alt: Proporciona texto alternativo apropiado para cada imagen
Las etiquetas se posicionan con CSS position: absolute en las esquinas de la imagen, con z-index ajustado para permanecer visibles durante la operación del slider. El tamaño de fuente de las etiquetas se escala con clamp(0.75rem, 2vw, 1rem) para un comportamiento responsivo natural.
Implementación CSS - Optimización de rendimiento con clip-path
La implementación con clip-path ofrece el mejor rendimiento de arrastre porque los cambios en clip-path no activan recálculo de layout - solo se necesitan actualizaciones de capas compuestas, habilitando la aceleración GPU.
CSS principal:
.comparison-slider { position: relative; overflow: hidden; cursor: col-resize; } .comparison-slider__before, .comparison-slider__after { position: absolute; inset: 0; } .comparison-slider__before img, .comparison-slider__after img { display: block; width: 100%; height: 100%; object-fit: cover; } .comparison-slider__after { clip-path: inset(0 0 0 50%); } .comparison-slider__handle { position: absolute; top: 0; bottom: 0; left: 50%; width: 4px; background: white; transform: translateX(-50%); box-shadow: 0 0 8px rgba(0,0,0,0.3); }
Puntos de optimización de rendimiento:
- will-change: clip-path: Establecido en el elemento After para indicar intención de animación. Añadir al inicio del arrastre, eliminar al final para evitar sobrecarga constante de memoria
- contain: layout: Establecido en el contenedor para indicar que los cambios internos no afectan el layout externo
- Promoción de capa GPU:
transform: translateZ(0)promueve el control a una capa compuesta independiente, evitando repintados durante el movimiento
Diseño responsivo: Establece aspect-ratio: 16/9 en el contenedor con ancho 100% para cálculo automático de altura. Las imágenes usan object-fit: cover para manejar relaciones de aspecto no coincidentes de forma elegante.
Implementación JavaScript - Manejo de arrastre y procesamiento de eventos
Implementa la interacción del slider con JavaScript soportando tanto ratón como toque mediante la API unificada de Pointer Events.
Puntos principales de implementación:
- Pointer Events API: Usa
pointerdown,pointermove,pointeruppara manejar ratón, toque y lápiz de forma uniforme.setPointerCapture()asegura que los eventos continúen incluso cuando el puntero se mueve fuera del control - Cálculo de posición:
event.clientX - container.getBoundingClientRect().leftda la posición relativa dentro del contenedor, dividida por el ancho del contenedor para obtener el porcentaje (0-100) - requestAnimationFrame: Como
pointermovese dispara a alta frecuencia, agrupa las actualizaciones del DOM a una vez por frame mediante requestAnimationFrame - Limitación de bordes:
Math.max(0, Math.min(100, percent))previene que el slider exceda los límites del contenedor
Implementación de operación por teclado:
handle.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') updatePosition(currentPercent - 1); if (e.key === 'ArrowRight') updatePosition(currentPercent + 1); if (e.key === 'Home') updatePosition(0); if (e.key === 'End') updatePosition(100); });
Función de actualización del DOM:
function updatePosition(percent) { percent = Math.max(0, Math.min(100, percent)); afterEl.style.clipPath = `inset(0 0 0 ${percent}%)`; handleEl.style.left = `${percent}%`; handleEl.setAttribute('aria-valuenow', Math.round(percent)); }
Consideraciones para dispositivos táctiles: Establece touch-action: none en el contenedor para prevenir el comportamiento de desplazamiento predeterminado. Usa touch-action: pan-y en su lugar si el desplazamiento vertical debe permanecer habilitado mientras solo el movimiento horizontal activa el slider.
Funciones avanzadas - Animación, carga diferida, múltiples instancias
Mejora la experiencia del usuario con funciones avanzadas más allá de la implementación básica.
Animación inicial (onboarding):
- Reproduce una sutil animación de oscilación al cargar la página para indicar interactividad
- CSS:
@keyframes hint { 0%,100% { left: 50% } 25% { left: 35% } 75% { left: 65% } } - Detiene la animación en la primera interacción del usuario (establece
animation: none) - Usa IntersectionObserver para iniciar la animación solo cuando es visible en el viewport
Carga diferida de imágenes:
- Los sliders de comparación típicamente están debajo del pliegue - usa
loading="lazy"para carga diferida - Muestra un marcador de posición que preserva la relación de aspecto (caja gris) antes de la carga para prevenir CLS
- Habilita el slider solo después de que ambas imágenes carguen:
Promise.all([img1.decode(), img2.decode()]).then(enableSlider)
Gestión de múltiples instancias:
- Al colocar múltiples sliders por página, gestiona cada instancia independientemente
- Implementación basada en clases:
class ComparisonSlider { constructor(el) { this.container = el; this.init(); } } - Inicialización:
document.querySelectorAll('.comparison-slider').forEach(el => new ComparisonSlider(el)) - Prevención de fugas de memoria: Implementa un método
destroy()para eliminar event listeners cuando los componentes se desmontan en SPAs
Slider vertical: Usa clip-path: inset(50% 0 0 0) para división superior/inferior con control horizontal para comparaciones verticales. Un atributo data-direction="vertical" permite cambiar la dirección para máxima versatilidad.
Comparación de bibliotecas y criterios de selección
Las bibliotecas existentes ofrecen alternativas a la implementación personalizada. Elige según los requisitos del proyecto.
Comparación de bibliotecas principales:
- img-comparison-slider (Web Component): 3.5KB gzipped. Web Component independiente del framework con etiqueta declarativa
<img-comparison-slider>. Soporte completo de teclado y accesibilidad. El más ligero y recomendado - TwentyTwenty (jQuery): Biblioteca legacy dependiente de jQuery. Funcional pero inapropiada para proyectos sin jQuery. 5KB + jQuery 87KB de bundle
- Cocoen: Implementación en JavaScript vanilla. 2KB gzipped, ligero con soporte táctil. Carece de funciones de accesibilidad (sin atributos ARIA)
- React Compare Image: Componente específico de React con soporte TypeScript. Personalización basada en Props pero limitado a proyectos React
Criterios de selección: prioridad en tamaño de bundle favorece img-comparison-slider (3.5KB) o Cocoen (2KB); prioridad en accesibilidad favorece img-comparison-slider (ARIA completo); proyectos React usan React Compare Image; máxima personalización requiere implementación propia.
Elige implementación propia cuando: integres completamente en un sistema de diseño, necesites interacciones especiales (zoom de pellizco, rotación, comparación de 3+ imágenes), requisitos estrictos de rendimiento, o cuando las bibliotecas existentes tengan accesibilidad insuficiente. Siempre mide el impacto en Core Web Vitals y optimiza imágenes (WebP/AVIF, dimensionado adecuado) para evitar retrasos en LCP.