UI Components Medium
Diff Slider
Before/after image comparison slider — drag the handle to reveal the difference. Touch support included.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 640px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.hint {
font-size: 0.75rem;
color: #334155;
text-align: center;
margin-top: 1rem;
}
/* ── Diff Slider container ── */
.diff-slider {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 1rem;
overflow: hidden;
user-select: none;
cursor: ew-resize;
border: 1px solid rgba(255, 255, 255, 0.08);
touch-action: pan-y;
}
/* ── Layers ── */
.diff-layer {
position: absolute;
inset: 0;
}
/* Before image: dark monochrome gradient */
.diff-image--before {
width: 100%;
height: 100%;
background: radial-gradient(ellipse at 30% 50%, #1e293b 0%, #0f172a 50%, #020617 100%);
}
/* After image: vivid colorful gradient */
.diff-image--after {
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at 70% 40%,
#7c3aed 0%,
#2563eb 35%,
#0891b2 60%,
#059669 85%,
#065f46 100%
);
}
/* After layer clip — JS updates --split CSS variable */
.diff-layer--after {
clip-path: inset(0 calc(100% - var(--split, 50%)) 0 0);
transition: clip-path 0s;
}
/* ── Labels ── */
.diff-label {
position: absolute;
bottom: 0.875rem;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.3rem 0.65rem;
border-radius: 999px;
backdrop-filter: blur(8px);
}
.diff-label--before {
left: 0.875rem;
background: rgba(0, 0, 0, 0.5);
color: #94a3b8;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.diff-label--after {
right: 0.875rem;
background: rgba(124, 58, 237, 0.3);
color: #c4b5fd;
border: 1px solid rgba(124, 58, 237, 0.4);
}
/* ── Divider line ── */
.diff-divider {
position: absolute;
top: 0;
bottom: 0;
left: var(--split, 50%);
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
z-index: 10;
}
.diff-line {
width: 2px;
flex: 1;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
}
/* ── Handle ── */
.diff-handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
background: #fff;
color: #0f172a;
border: none;
cursor: ew-resize;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 0 3px rgba(255, 255, 255, 0.2);
transition: transform 0.15s ease, box-shadow 0.15s ease;
z-index: 11;
}
.diff-handle:hover,
.diff-handle:focus-visible {
transform: translate(-50%, -50%) scale(1.1);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6), 0 0 0 4px rgba(255, 255, 255, 0.35);
outline: none;
}
/* Active drag state */
.diff-slider.dragging .diff-handle {
transform: translate(-50%, -50%) scale(1.12);
}
.diff-slider.dragging .diff-layer--after {
transition: none;
}(function () {
"use strict";
const slider = document.querySelector(".diff-slider");
const handle = slider && slider.querySelector(".diff-handle");
if (!slider || !handle) return;
let isDragging = false;
let split = 50; // percentage
function setSplit(pct) {
split = Math.max(2, Math.min(98, pct));
slider.style.setProperty("--split", split + "%");
handle.setAttribute("aria-valuenow", Math.round(split));
}
function getPercent(clientX) {
const rect = slider.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
}
// Mouse events
handle.addEventListener("mousedown", function (e) {
e.preventDefault();
isDragging = true;
slider.classList.add("dragging");
});
document.addEventListener("mousemove", function (e) {
if (!isDragging) return;
setSplit(getPercent(e.clientX));
});
document.addEventListener("mouseup", function () {
if (!isDragging) return;
isDragging = false;
slider.classList.remove("dragging");
});
// Touch events
handle.addEventListener(
"touchstart",
function (e) {
e.preventDefault();
isDragging = true;
slider.classList.add("dragging");
},
{ passive: false }
);
document.addEventListener(
"touchmove",
function (e) {
if (!isDragging) return;
setSplit(getPercent(e.touches[0].clientX));
},
{ passive: true }
);
document.addEventListener("touchend", function () {
if (!isDragging) return;
isDragging = false;
slider.classList.remove("dragging");
});
// Keyboard support
handle.addEventListener("keydown", function (e) {
const step = e.shiftKey ? 10 : 2;
if (e.key === "ArrowLeft") {
e.preventDefault();
setSplit(split - step);
}
if (e.key === "ArrowRight") {
e.preventDefault();
setSplit(split + step);
}
if (e.key === "Home") {
e.preventDefault();
setSplit(2);
}
if (e.key === "End") {
e.preventDefault();
setSplit(98);
}
});
// Click on slider background to jump
slider.addEventListener("click", function (e) {
if (e.target === handle) return;
setSplit(getPercent(e.clientX));
});
// Initialize
setSplit(50);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diff Slider</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Diff Slider</h1>
<p class="demo-sub">Drag the handle to compare before and after.</p>
<div
class="diff-slider"
role="img"
aria-label="Before and after image comparison"
>
<!-- Before layer (dark/desaturated) -->
<div class="diff-layer diff-layer--before" aria-hidden="true">
<div class="diff-image diff-image--before"></div>
<span class="diff-label diff-label--before">Before</span>
</div>
<!-- After layer (colorful) — clipped from the right -->
<div class="diff-layer diff-layer--after" aria-hidden="true">
<div class="diff-image diff-image--after"></div>
<span class="diff-label diff-label--after">After</span>
</div>
<!-- Divider + handle -->
<div class="diff-divider" aria-hidden="true">
<div class="diff-line"></div>
<button
class="diff-handle"
aria-label="Drag to compare before and after"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
role="slider"
tabindex="0"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8.59 16.59L7.17 18 2 12l5.17-6 1.42 1.41L5.83 12l2.76 3.17zM16.83 7.17L15.41 6 20 12l-5.17 6-1.41-1.41L16.17 12l-2.76-3.17z"/>
</svg>
</button>
</div>
</div>
<p class="hint">← Drag handle or use arrow keys →</p>
</div>
<script src="script.js"></script>
</body>
</html>Diff Slider
A before/after image comparison slider. Drag the center handle to reveal how much of each image is visible. Works with both mouse and touch input.
How it works
The “after” image is clipped using clip-path: inset(0 X% 0 0) where X is derived from the handle’s horizontal position. Moving the handle updates the clip in real time via JavaScript.
Features
- Mouse drag + touch drag support
- Keyboard accessible (left/right arrow keys)
- CSS-only handle design (no image assets required)
- Works with any images of the same dimensions
prefers-reduced-motionrespected
Usage
Wrap two <img> elements (or any content) inside .diff-slider. The first child is the “before” layer, the second is the “after” layer.