Interactive Image Comparison Slider
A sleek before/after image comparison slider with smooth drag interactions, touch device support, custom glassmorphic handle design, and an automatic intro animation.
Give users a powerful, interactive way to view “before and after” states with this premium image comparison slider. Designed to feel buttery smooth on both desktop and mobile, it features a custom glassmorphic drag handle, intelligent touch support, and an auto-slide intro animation to guide users on how to use it.
Design Highlights
- Fluid Dragging: Uses optimized JavaScript event listeners (
pointerdown,pointermove) to ensure smooth tracking without lag, working perfectly on touchscreens and mice. - Auto-Slide Intro: On page load, the slider gently moves back and forth to naturally hint to the user that it is interactive.
- Custom Glass Handle: A premium frosted glass draggable node with a modern arrow icon, replacing default native input sliders.
- CSS
clip-path: Uses hardware-accelerated CSSclip-pathclipping to elegantly reveal the “before” image layered exactly over the “after” image. - Responsive: Scales beautifully to fit any container while perfectly maintaining the images’ aspect ratios without distortion.
Code Snippet
HTML Structure
The markup stacks two images on top of each other and uses a standard range input invisibly laid over the custom handle to provide robust accessibility and native drag physics.
<div class="comparison-slider" id="imageCompare">
<!-- Base 'After' Image -->
<img src="/path/to/after.jpg" alt="After" class="slider-img after-img" />
<!-- Overlay 'Before' Image (Clipped) -->
<img src="/path/to/before.jpg" alt="Before" class="slider-img before-img" id="beforeImage" />
<!-- Custom Slider Handle UI -->
<div class="slider-handle" id="sliderHandle">
<div class="handle-line"></div>
<div class="handle-button">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</div>
</div>
<!-- Native Range Input (Hidden but accessible) -->
<input type="range" min="0" max="100" value="50" class="slider-input" id="sliderInput" aria-label="Percentage of before photo shown" />
</div>
CSS Styles
The magic happens by dynamically updating a CSS variable (--position) that controls both the clip-path of the top image and the horizontal position of the handle.
.comparison-slider {
--position: 50%;
position: relative;
width: 100%;
max-width: 800px;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.slider-img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.after-img {
position: relative;
}
/* The magic clipping */
.before-img {
position: absolute;
top: 0;
left: 0;
/* Clips the right side of the top image dynamically */
clip-path: polygon(0 0, var(--position) 0, var(--position) 100%, 0 100%);
}
/* Custom Visual Handle */
.slider-handle {
position: absolute;
top: 0;
bottom: 0;
left: var(--position);
width: 2px;
background: white;
transform: translateX(-50%);
pointer-events: none;
z-index: 10;
}
.handle-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
border: 2px solid white;
display: flex;
align-items: center;
justify-content: center;
}
/*
* Invisible Range Input over the whole container
* This captures all touch & drag events natively for free!
*/
.slider-input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: ew-resize;
z-index: 20;
margin: 0;
}
JavaScript Logic
We map the invisible <input type="range"> value to the CSS --position variable. Simple, accessible, and performant.
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('imageCompare');
const slider = document.getElementById('sliderInput');
// Update CSS position based on slider
const updateSlider = (val) => {
container.style.setProperty('--position', `${val}%`);
};
// Listen to input events
slider.addEventListener('input', (e) => {
updateSlider(e.target.value);
});
// Optional: Auto-slide intro animation
let animationFrames = 0;
const startAnim = () => {
if(animationFrames > 50) return; // Stop after a bit
animationFrames++;
// Math.sin creates a smooth back-and-forth oscillation
const pos = 50 + Math.sin(animationFrames * 0.1) * 10;
updateSlider(pos);
slider.value = pos;
requestAnimationFrame(startAnim);
};
// startAnim(); // Uncomment to run
});
By utilizing a hidden native <input type="range">, we completely bypass the need for complex, buggy mousedown and mousemove absolute coordinate tracking, providing the absolute best experience out-of-the-box.