How to Build Comparison Sliders Using `clip-path`: A Complete Implementation Guide

Use clip-path: inset() to create performant, GPU-accelerated comparison sliders by clipping a top image over a base image and updating the inset value based on drag position.

A comparison slider—sometimes called a "before/after slider"—is a common UI pattern for revealing visual differences between two images. This guide teaches you how to build one using the clip-path CSS property, based on the implementation patterns documented in the emilkowalski/skills repository.

What Is a Comparison Slider with clip-path?

A comparison slider overlays two images and lets users drag a divider to reveal more of either image. The clip-path approach uses CSS inset clipping to mask portions of the top image, eliminating the need for complex DOM structures or JavaScript-heavy canvas operations.

According to skills/emil-design-eng/SKILL.md, the recommended technique uses clip-path: inset(0 50% 0 0) for an initial 50/50 split, with the second value (right inset) animated during user interaction.

Why clip-path Beats Alternative Approaches

  • Hardware acceleration – Browsers render clipped layers on the GPU, delivering 60fps animations without layout thrashing.
  • Minimal DOM footprint – Only two images and a handle element; no wrapper containers or duplicate image copies needed.
  • Clean separation of concerns – CSS handles the visual clipping, JavaScript only updates percentage values.
  • Broad browser support – clip-path with inset() works in all modern browsers; overflow: hidden provides a safe fallback.

The skills/review-animations/STANDARDS.md file specifies standard transition timing for clip-path animations: 200ms ease-out on release and 2s linear on press for smooth, responsive feedback.

Core Architecture

The pattern requires three elements working together:

  1. Base image – Positioned normally, fully visible at all times.
  2. Top image – Absolutely positioned over the base, clipped with clip-path: inset().
  3. Draggable handle – Tracks user input and drives the clip-path update.

As the handle moves horizontally, JavaScript calculates a percentage from 0 to 100 and applies it to the top image's right inset value.

Complete Implementation

HTML Structure

<div class="compare-slider">
  <img src="before.jpg" alt="Before" class="cmp-img base-img">
  <img src="after.jpg" alt="After" class="cmp-img top-img">
  <div class="handle" aria-label="Drag to compare images"></div>
</div>

CSS Styling

.compare-slider {
  position: relative;
  width: 100%;
  max-width: 800px;
  overflow: hidden;  /* Fallback for older browsers */
}

.cmp-img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
}

/* Base image: standard flow */
.base-img {
  position: relative;
}

/* Top image: clipped overlay */
.top-img {
  position: absolute;
  inset: 0;
  clip-path: inset(0 50% 0 0);  /* Show left 50% initially */
  transition: clip-path 150ms ease-out;
  pointer-events: none;
}

/* Draggable divider */
.handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 4px;
  background: #fff;
  cursor: ew-resize;
  transform: translateX(-50%);
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  z-index: 10;
}

JavaScript Interaction

const slider = document.querySelector('.compare-slider');
const topImg = slider.querySelector('.top-img');
const handle = slider.querySelector('.handle');

let dragging = false;

function updateClipPosition(clientX) {
  const rect = slider.getBoundingClientRect();
  const offsetX = Math.max(0, Math.min(clientX - rect.left, rect.width));
  const percent = (offsetX / rect.width) * 100;
  
  // Inset values: top right bottom left
  // Right inset shrinks as handle moves right, revealing more of base image
  topImg.style.clipPath = `inset(0 ${100 - percent}% 0 0)`;
  handle.style.left = `${percent}%`;
}

// Pointer events for mouse + touch unified handling
handle.addEventListener('pointerdown', (e) => {
  dragging = true;
  slider.setPointerCapture(e.pointerId);
  topImg.style.transition = 'none';  // Disable during drag for responsiveness
});

slider.addEventListener('pointermove', (e) => {
  if (!dragging) return;
  updateClipPosition(e.clientX);
});

slider.addEventListener('pointerup', (e) => {
  if (!dragging) return;
  dragging = false;
  slider.releasePointerCapture(e.pointerId);
  topImg.style.transition = 'clip-path 200ms ease-out';  // Re-enable per standards
});

// Keyboard accessibility
handle.setAttribute('tabindex', '0');
handle.addEventListener('keydown', (e) => {
  const rect = slider.getBoundingClientRect();
  const currentLeft = parseFloat(handle.style.left) || 50;
  let newPercent = currentLeft;
  
  if (e.key === 'ArrowLeft') newPercent = Math.max(0, currentLeft - 5);
  if (e.key === 'ArrowRight') newPercent = Math.min(100, currentLeft + 5);
  
  topImg.style.clipPath = `inset(0 ${100 - newPercent}% 0 0)`;
  handle.style.left = `${newPercent}%`;
});

Understanding the clip-path: inset() Syntax

The inset() function defines a rectangle by cutting away from each edge:

clip-path: inset(top right bottom left);

For horizontal comparison sliders:

Inset Value Effect
inset(0 75% 0 0) Shows left 25% of top image
inset(0 50% 0 0) Shows left 50% (default position)
inset(0 25% 0 0) Shows left 75% of top image
inset(0 0 0 0) Shows full top image (100% reveal)

The JavaScript calculates 100 - percent for the right value because inset measures removal from that edge, not width.

Vertical Comparison Sliders

Rotate the pattern 90 degrees by swapping the inset axis:

/* Vertical clip: top value animates instead of right */
.top-img-vertical {
  clip-path: inset(50% 0 0 0);  /* Show bottom half initially */
}
// Update top inset based on Y position
function updateVerticalClip(clientY) {
  const rect = slider.getBoundingClientRect();
  const offsetY = clientY - rect.top;
  const percent = (offsetY / rect.height) * 100;
  topImg.style.clipPath = `inset(${100 - percent}% 0 0 0)`;
}

Performance Optimization Tips

  • Disable transitions during drag – Set transition: none on pointerdown to eliminate frame drops from simultaneous animation and user input.
  • Use pointer-events: none on the clipped top image to prevent it from intercepting events meant for the handle.
  • Throttle resize handlers if recalculating bounds on window resize.
  • Prefer transform and opacity for any additional animations, as these composite most efficiently with clip-path.

Source File Reference

File Description
skills/emil-design-eng/SKILL.md Documents the clip-path: inset() pattern for comparison sliders, including syntax and use cases.
skills/review-animations/STANDARDS.md Defines standard transition timing: 200ms ease-out on release, 2s linear on press.

These files contain the authoritative guidelines that inform this implementation.

Summary

  • clip-path: inset() creates GPU-accelerated image reveals without extra DOM nodes.
  • Three elements handle the entire pattern: base image, clipped top image, and draggable handle.
  • Percent-based calculations map handle position to inset values for smooth, responsive interaction.
  • skills/emil-design-eng/SKILL.md and skills/review-animations/STANDARDS.md provide the canonical reference for this technique.

Frequently Asked Questions

Can clip-path comparison sliders work without JavaScript?

No—the dynamic clipping requires runtime position calculations. However, you can provide a static fallback using CSS-only clip-path values (e.g., inset(0 50% 0 0)) that displays a fixed 50/50 split when JavaScript is unavailable.

How does clip-path performance compare to width or transform approaches?

clip-path outperforms width-based solutions because it doesn't trigger layout recalculation. Unlike transform: scaleX(), clip-path actually masks content without distortion, making it semantically correct for "before/after" comparisons. The GPU acceleration rivals transform performance in modern browsers.

What browsers support clip-path: inset() for comparison sliders?

All evergreen browsers support inset() as of 2020. For legacy support, the overflow: hidden technique—wrapping the top image in a container and animating container width—provides a functional fallback that degrades gracefully.

How do I add labels or annotations to specific regions in a comparison slider?

Position absolutely positioned overlays as siblings to the images, outside the clipped element. Use pointer-events: none on annotations that shouldn't interfere with dragging, or add z-index layers above the handle for interactive markers that respond independently of the swipe gesture.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →