← Visual Library / Carousels

Before/after slider

Drag the handle left or right to reveal one image under another. Perfect for renovations, staging, retouching.

Carousels Built Tiny JS
Use when — you have a clear "before vs after" story to tell: renovation projects, virtual staging, retouching, weight loss, dental work. Strong emotional storytelling element. Always pair with a tight caption so the user knows what changed.

Live demo

Before
After

Drag the slider to reveal the renovation.

How it works

Two layers stacked. The "after" image has clip-path: inset(0 0 0 var(--split, 50%)) so it only shows from the split position onward. A range input is positioned over the whole thing — moving it sets a CSS variable --split on the parent, which animates the clip-path. The handle visual sits on top, decorative.

This is the rare case where a 4-line JS handler earns its keep: nothing CSS-only does smooth drag-tracking on the slider value. The JS is tiny enough that it doesn't violate the JS-free spirit — and the page still renders + reads fine without it (slider just doesn't move).

The code (HTML)

<div class="ba" id="ba">
  <div class="before"></div>
  <div class="after"></div>
  <div class="handle"></div>
  <input type="range" min="0" max="100" value="50"
         id="ba-range" aria-label="Reveal slider">
</div>

The code (CSS — the key bits)

.ba { position: relative; aspect-ratio: 4/3; }
.ba .before, .ba .after { position: absolute; inset: 0; }
.ba .after { clip-path: inset(0 0 0 var(--split, 50%)); }
.ba .handle { left: var(--split, 50%); }
.ba input[type="range"] { position: absolute; inset: 0; opacity: 0; cursor: ew-resize; }

The code (JS — 4 lines)

const ba = document.getElementById('ba');
const r  = document.getElementById('ba-range');
r.addEventListener('input', () => {
  ba.style.setProperty('--split', r.value + '%');
});

Variants worth considering

Vertical split — swap inset(0 0 0 var(--split)) for inset(var(--split) 0 0 0) and rotate the handle 90°. Good for portrait-shaped photos.

Tap to toggle — if you want a JS-free version, replace the range input with a checkbox + label that toggles --split between 0% and 100%. Lose the drag, gain pure CSS.

Animated reveal — on viewport entry, animate --split from 100% to 50% to grab attention. Pair with IntersectionObserver if you want it scroll-triggered.

Compatibility: clip-path + custom property in style attribute work everywhere. Range input is universal.