How to Build Comparison Sliders with clip-path
Use CSS clip-path: inset() to dynamically clip an overlay image, updating the right inset percentage based on drag position to create smooth, hardware-accelerated before-and-after comparisons.
The emilkowalski/skills repository documents a performant pattern for building comparison sliders using CSS clip-path. According to the design engineering guidelines in skills/emil-design-eng/SKILL.md, this approach leverages GPU acceleration to reveal image overlays without triggering expensive layout recalculations or paint operations.
Understanding the clip-path Architecture
The comparison slider architecture relies on a two-layer stack where clip-path controls visibility. As documented in skills/review-animations/STANDARDS.md (lines 135-136), clip-path: inset() serves as a "powerful animation tool" specifically suited for comparison sliders.
The technique works by placing a "before" image as the base layer and an "after" image positioned absolutely on top. The clip-path: inset(0 50% 0 0) property hides a portion of the overlay image—the four values represent top, right, bottom, and left insets. A value of 50% on the right side clips the overlay exactly in half, while 0% reveals the full image and 100% hides it completely.
Because clip-path is a layout-independent property, the browser can animate changes entirely on the GPU. This produces 60fps motion without JavaScript-driven reflows, making it ideal for drag-based interactions.
Building the Slider Structure
Stacking the Image Layers
Create a container with two images: the base image flows normally in the document, while the overlay uses absolute positioning to cover it completely.
<div class="compare">
<img src="before.jpg" alt="Before" class="compare__base">
<img src="after.jpg" alt="After" class="compare__overlay">
<div class="compare__handle" aria-label="Drag to compare"></div>
</div>
.compare {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
}
.compare__base {
width: 100%;
height: 100%;
object-fit: cover;
}
.compare__overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
clip-path: inset(0 50% 0 0); /* Start at 50% */
}
Positioning the Drag Handle
The handle sits at the clipping boundary, providing visual feedback and a draggable target. Position it absolutely to match the initial clip value.
.compare__handle {
position: absolute;
inset: 0;
width: 2px;
background: #fff;
left: 50%; /* Matches the 50% inset */
cursor: ew-resize;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
Implementation Examples
Pure CSS Foundation
For simple hover-based comparisons, use CSS transitions on the clip-path property. When the user hovers over the container, animate the inset to reveal the full overlay.
.compare__overlay {
clip-path: inset(0 50% 0 0);
transition: clip-path 0.3s ease-out;
}
.compare:hover .compare__overlay {
clip-path: inset(0 0 0 0); /* Reveal full image */
}
JavaScript Drag Implementation
For draggable sliders, update the clip-path in real-time based on pointer position. This follows the interaction pattern described in the repository's skill files.
const compare = document.querySelector('.compare');
const overlay = compare.querySelector('.compare__overlay');
const handle = compare.querySelector('.compare__handle');
function updateSlider(clientX) {
const rect = compare.getBoundingClientRect();
const percent = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
const insetRight = (1 - percent) * 100;
overlay.style.clipPath = `inset(0 ${insetRight}% 0 0)`;
handle.style.left = `${percent * 100}%`;
}
let dragging = false;
compare.addEventListener('pointerdown', (e) => {
dragging = true;
updateSlider(e.clientX);
});
window.addEventListener('pointermove', (e) => {
if (dragging) updateSlider(e.clientX);
});
window.addEventListener('pointerup', () => {
dragging = false;
});
The setPosition function calculates the horizontal percentage within the container, clamps it between 0 and 1, then maps that to the right inset value. As the user drags left to right, the clipPath string updates dynamically, revealing more of the overlay image.
Web Animations API Approach
According to skills/emil-design-eng/SKILL.md (lines 13-21), the Web Animations API (WAAPI) provides a declarative way to programmatically animate the clip path without continuous drag loops.
function animateReveal(targetPercent) {
const overlay = document.querySelector('.compare__overlay');
const current = overlay.style.clipPath || 'inset(0 50% 0 0)';
const target = `inset(0 ${100 - targetPercent}% 0 0)`;
overlay.animate([
{ clipPath: current },
{ clipPath: target }
], {
duration: 600,
easing: 'cubic-bezier(0.77, 0, 0.175, 1)'
}).onfinish = () => {
overlay.style.clipPath = target;
};
}
This method creates a smooth transition between any two clip states, useful for programmatic comparisons or "reveal" buttons.
Accessibility and Performance Considerations
Respect user motion preferences by providing a static fallback. The repository's accessibility notes in skills/emil-design-eng/SKILL.md (lines 27-31) recommend disabling animations when prefers-reduced-motion is set.
@media (prefers-reduced-motion: reduce) {
.compare__overlay {
transition: none !important;
clip-path: inset(0 25% 0 0) !important; /* Static 25% reveal */
}
.compare__handle {
display: none;
}
}
The clip-path property enjoys excellent browser support and animates efficiently because it operates on the compositor thread. Unlike width or left changes, modifying clip-path does not trigger layout recalculations or repaints, ensuring consistent frame rates even on lower-end devices.
Summary
- Stack two images: Place the "after" image absolutely on top of the "before" image.
- Use
clip-path: inset(): Control visibility by adjusting the right inset value from 0% (full reveal) to 100% (fully hidden). - Update on drag: Map the mouse or touch position to the inset percentage for real-time comparisons.
- Leverage GPU acceleration: The browser animates
clip-pathon the compositor thread for smooth 60fps performance. - Reference: Implementation details are documented in
skills/emil-design-eng/SKILL.mdandskills/review-animations/STANDARDS.md.
Frequently Asked Questions
Why use clip-path instead of width or opacity?
clip-path creates a hard edge between two images without affecting layout. Unlike changing width, which triggers layout recalculations, or opacity, which creates ghosting effects, clip-path simply masks the element. As noted in skills/review-animations/STANDARDS.md, this property animates on the GPU, providing smoother performance than dimension-based alternatives.
How do I make the comparison slider accessible?
Include ARIA labels on the drag handle to describe its function, and respect prefers-reduced-motion by providing a static view. According to skills/emil-design-eng/SKILL.md (lines 27-31), you should disable the drag interaction and display a fixed comparison state when users have reduced motion enabled.
Can I animate the clip-path transition?
Yes. The clip-path property supports CSS transitions and the Web Animations API. You can animate between different inset() values to create smooth reveals. The repository recommends using cubic-bezier easing functions for natural motion, as implemented in the WAAPI example (lines 13-21).
Does this technique work with content other than images?
Absolutely. The clip-path approach works on any rectangular DOM element, including video elements, divs with background images, or complex component layouts. The technique documented in skills/emil-design-eng/SKILL.md specifically mentions that clip-path works on "any rectangular element and does not require extra DOM nodes."
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →