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

> Build performant comparison sliders using clip-path. Learn how to clip images with inset() and update drag position for GPU-accelerated effects.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-01

---

**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`](https://github.com/emilkowalski/skills/blob/main/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`](https://github.com/emilkowalski/skills/blob/main/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

```html
<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

```css
.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

```javascript
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:

```css
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:

```css
/* Vertical clip: top value animates instead of right */
.top-img-vertical {
  clip-path: inset(50% 0 0 0);  /* Show bottom half initially */
}

```

```javascript
// 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`](https://github.com/emilkowalski/skills/blob/main/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`](https://github.com/emilkowalski/skills/blob/main/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`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)** and **[`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/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.