Implementing Before/After Image Comparison Sliders - UI Design and Optimization
Image Comparison Slider Use Cases and Design Requirements
Image comparison sliders (Before/After Sliders) overlay two images, allowing users to drag a divider to visually inspect differences. Applications include photo editing before/after, compression quality comparison, web design iterations, and medical imaging temporal changes.
Design requirements:
- Intuitive interaction: Users must understand the operation without instructions. The handle must visually communicate draggability
- Responsive design: Support both desktop mouse and mobile touch interactions. Component scales appropriately with viewport width
- Performance: Maintain 60fps during dragging. Minimize repaints and leverage GPU acceleration
- Accessibility: Support keyboard operation (arrow keys) and communicate state to screen readers via ARIA attributes
- Image synchronization: Both images must perfectly align in position and size. Define handling for mismatched aspect ratios
Three main implementation approaches exist: CSS clip-path, overflow: hidden with width control, and Canvas rendering. Each has tradeoffs - this article focuses on the clip-path approach for its superior performance characteristics.
HTML Structure and Semantics - Accessible Markup
The HTML structure must consider semantics and accessibility, ensuring screen reader users understand the comparison intent and can operate via keyboard.
Recommended HTML structure:
<div class="comparison-slider" role="group" aria-label="Image comparison"><div class="comparison-slider__before"><img src="before.webp" alt="Before processing" /><span class="comparison-slider__label">Before</span></div><div class="comparison-slider__after"><img src="after.webp" alt="After processing" /><span class="comparison-slider__label">After</span></div><div class="comparison-slider__handle" role="slider" aria-label="Comparison position" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" tabindex="0"></div></div>
Markup considerations:
- role="group": Set on container to indicate related elements
- role="slider": Set on handle to communicate slider control to screen readers
- aria-valuemin/max/now: Communicate current position as percentage, updated dynamically via JavaScript
- tabindex="0": Makes handle focusable for keyboard operation
- alt attributes: Provide appropriate alternative text for each image
Labels are positioned with CSS position: absolute at image corners, with z-index adjusted to remain visible during slider operation. Label font size scales with clamp(0.75rem, 2vw, 1rem) for natural responsive behavior.
CSS Implementation - Performance Optimization with clip-path
The clip-path implementation offers the best drag performance because clip-path changes don't trigger layout recalculation - only composite layer updates are needed, enabling GPU acceleration.
Core CSS:
.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); }
Performance optimization points:
- will-change: clip-path: Set on the After element to hint animation intent. Add on drag start, remove on drag end to avoid constant memory overhead
- contain: layout: Set on container to indicate internal changes don't affect external layout
- GPU layer promotion:
transform: translateZ(0)promotes the handle to an independent composite layer, avoiding repaints during movement
Responsive design: Set aspect-ratio: 16/9 on the container with 100% width for automatic height calculation. Images use object-fit: cover to handle mismatched aspect ratios gracefully.
JavaScript Implementation - Drag Handling and Event Processing
Implement slider interaction with JavaScript supporting both mouse and touch via the unified Pointer Events API.
Core implementation points:
- Pointer Events API: Use
pointerdown,pointermove,pointerupto handle mouse, touch, and pen uniformly.setPointerCapture()ensures events continue even when pointer moves outside the handle - Position calculation:
event.clientX - container.getBoundingClientRect().leftgives relative position within container, divided by container width for percentage (0-100) - requestAnimationFrame: Since
pointermovefires at high frequency, batch DOM updates to once per frame via requestAnimationFrame - Boundary clamping:
Math.max(0, Math.min(100, percent))prevents slider from exceeding container bounds
Keyboard operation implementation:
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); });
DOM update function:
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)); }
Touch device considerations: Set touch-action: none on the container to prevent default scroll behavior. Use touch-action: pan-y instead if vertical scrolling should remain enabled while only horizontal movement triggers the slider.
Advanced Features - Animation, Lazy Loading, Multiple Instances
Enhance user experience with advanced features beyond the basic implementation.
Initial animation (onboarding):
- Play a subtle oscillation animation on page load to indicate interactivity
- CSS:
@keyframes hint { 0%,100% { left: 50% } 25% { left: 35% } 75% { left: 65% } } - Stop animation on first user interaction (set
animation: none) - Use IntersectionObserver to start animation only when visible in viewport
Image lazy loading:
- Comparison sliders are typically below the fold - use
loading="lazy"for deferred loading - Display aspect-ratio-preserving placeholder (gray box) before load to prevent CLS
- Enable slider only after both images load:
Promise.all([img1.decode(), img2.decode()]).then(enableSlider)
Multiple instance management:
- When placing multiple sliders per page, manage each instance independently
- Class-based implementation:
class ComparisonSlider { constructor(el) { this.container = el; this.init(); } } - Initialization:
document.querySelectorAll('.comparison-slider').forEach(el => new ComparisonSlider(el)) - Memory leak prevention: Implement
destroy()method to remove event listeners when components unmount in SPAs
Vertical slider: Use clip-path: inset(50% 0 0 0) for top/bottom split with horizontal handle for vertical comparisons. A data-direction="vertical" attribute enables direction switching for maximum versatility.
Library Comparison and Selection Criteria
Existing libraries offer alternatives to custom implementation. Choose based on project requirements.
Major library comparison:
- img-comparison-slider (Web Component): 3.5KB gzipped. Framework-agnostic Web Component with declarative
<img-comparison-slider>tag. Full keyboard and accessibility support. Lightest and most recommended - TwentyTwenty (jQuery): Legacy jQuery-dependent library. Functional but inappropriate for jQuery-free projects. 5KB + jQuery 87KB bundle
- Cocoen: Vanilla JavaScript implementation. 2KB gzipped, lightweight with touch support. Lacks accessibility features (no ARIA attributes)
- React Compare Image: React-specific component with TypeScript support. Props-based customization but limited to React projects
Selection criteria: bundle size priority favors img-comparison-slider (3.5KB) or Cocoen (2KB); accessibility priority favors img-comparison-slider (full ARIA); React projects use React Compare Image; maximum customization requires custom implementation.
Choose custom implementation when: integrating fully into a design system, needing special interactions (pinch zoom, rotation, 3+ image comparison), strict performance requirements, or when existing libraries have insufficient accessibility. Always measure Core Web Vitals impact and optimize images (WebP/AVIF, proper sizing) to avoid LCP delays.