GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Before After Slider UIs — Drag to Compare

A before after slider stacks two photos and splits them at a line you can move, so before and after show side by side.

Auto-plays · click a tile to jump to its section · all nine in one zip

These nine follow the order the component actually gets used in, not a "prettiest first" list: click once to see it move, the way any before after slider online demo starts, drag it continuously, try it on a phone, check it with only a keyboard, flip the axis to vertical, add labels, add a caption, zoom in, then finally line several pairs up on one page. Hanging many photos on a wall instead of stacking two is 9 CSS Image Gallery Grid Effects, and handling their order by mouse is Portfolio Gallery HTML CSS.

01Click-to-reveal comparison slider

What a before after slider ui shows on first click

Click anywhere on the photo and the divider jumps straight to that x-position.

Click-to-reveal comparison slider
Open in a new tab

01 · Click-to-reveal comparison slider — Measured area 12.083% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path:inset()CSS 커스텀 프로퍼티click→jump

Where to look in the code

It's the simplest version to try first, since there's no drag involved at all.

el.addEventListener("click", function (e) {
  el.classList.remove("is-demo");
  var r = el.getBoundingClientRect();
  var pct = ((e.clientX - r.left) / r.width) * 100;
  el.style.setProperty("--reveal", Math.max(0, Math.min(100, pct)) + "%");
});

02Drag-handle comparison slider

What happens on screen

Drag the round handle back and forth and it follows the pointer in real time.

Drag-handle comparison slider
Open in a new tab

02 · Drag-handle comparison slider — Measured area 13.55% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointer 드래그@property 변수handle translateX

How to read the code

This is the version most product pages actually ship.

grip.addEventListener("pointerdown", function (e) {
  el.classList.remove("is-demo");
  dragging = true;
  grip.setPointerCapture(e.pointerId);
  set(fromX(e.clientX));
});
grip.addEventListener("pointermove", function (e) {
  if (dragging) set(fromX(e.clientX));
});

03Touch-swipe comparison slider

What moves

The whole slider is the touch target, not just a thin handle, and the grip grows to 48px so a fingertip lands on it reliably.

Touch-swipe comparison slider
Open in a new tab

03 · Touch-swipe comparison slider — Measured area 12.826% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

touch-action:nonesetPointerCapture48px 터치 영역

What to change

.ba {
  touch-action: none;
  cursor: grab;
}
.ba__grip {
  width: $sp-12;   // 48px — a realistic minimum touch target
  height: $sp-12;
  border-radius: $r-pill;
}

04Keyboard-accessible comparison slider

What you see

Tab onto the slider and arrow keys move it 5% at a time; Home and End jump to either end.

Keyboard-accessible comparison slider
Open in a new tab

04 · Keyboard-accessible comparison slider — Measured area 13.651% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

role=slideraria-valuenow방향키 5% 이동

Where to look in the code

No mouse required to compare before and after.

el.addEventListener("keydown", function (e) {
  el.classList.remove("is-demo");
  if (e.key === "ArrowLeft") { set(cur() - 5); e.preventDefault(); }
  else if (e.key === "ArrowRight") { set(cur() + 5); e.preventDefault(); }
  else if (e.key === "Home") { set(0); e.preventDefault(); }
  else if (e.key === "End") { set(100); e.preventDefault(); }
});

05Vertical comparison slider

What happens on screen

The divider moves up and down instead of left and right, so a tall vertical photo doesn't need to be rotated to compare.

Vertical comparison slider
Open in a new tab

05 · Vertical comparison slider — Measured area 8.635% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path 세로축--reveal 변수vh 무대

How to read the code

.ba__before {
  clip-path: inset(0 0 calc(100% - var(--reveal)) 0);
}
.ba__handle {
  top: var(--reveal);
  height: $sp-1;
  transform: translateY(-50%);
}

06Before/after label badges

What moves

Fixed pill badges in the top corners fade in and out as the divider passes, so it's never ambiguous which side is which.

Before/After label badges
Open in a new tab

06 · Before/After label badges — Measured area 13.842% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

calc() opacity$r-pill 배지position:absolute 라벨

What to change

function set(pct) {
  pct = Math.max(0, Math.min(100, pct));
  // one property for the position, one plain 0-1 number for opacity math
  el.style.setProperty("--reveal", pct + "%");
  el.style.setProperty("--reveal-num", (pct / 100).toFixed(3));
}

07Caption-overlay comparison slider

What you see

A translucent gradient band sits along the bottom with a caption like "3 weeks later," spelling out in words what the photos compare.

Caption-overlay comparison slider
Open in a new tab

07 · Caption-overlay comparison slider — Measured area 11.721% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

linear-gradient 오버레이clip-path: insetcaption 텍스트

Where to look in the code

.ba__caption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: $sp-6 $sp-4 $sp-3;
  background: linear-gradient(to top, rgba(23,20,26,.78) 0%, rgba(23,20,26,0) 100%);
  color: $color;
  font: 700 13px/1.3 system-ui, sans-serif;
}

08Zoom-synced comparison slider

What happens on screen

Scrolling the wheel zooms both photos together at the same scale and center point, so a close look at fine detail never drifts out of sync.

Zoom-synced comparison slider
Open in a new tab

08 · Zoom-synced comparison slider — Measured area 15.303% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform:scale() 동기화wheel 줌transform-origin 고정

How to read the code

el.addEventListener("wheel", function (e) {
  el.classList.remove("is-demo");
  e.preventDefault();
  var cur = parseFloat(getComputedStyle(el).getPropertyValue("--zoom")) || 1;
  setZoom(cur + (e.deltaY < 0 ? 0.15 : -0.15));
}, { passive: false });

09Multi-pair comparison carousel

What moves

Three sliders sit side by side in a scroll-snap carousel, and IntersectionObserver enables dragging only on whichever pane is centered in view.

Multi-pair comparison carousel
Open in a new tab

09 · Multi-pair comparison carousel — Measured area 13.271% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

scroll-snap-type:xIntersectionObserver슬라이더 다중 관리

What to change

var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (en) {
    en.target.classList.toggle("is-active", en.intersectionRatio > 0.6);
  });
}, { root: track, threshold: [0.6] });
panes.forEach(function (p) { io.observe(p); });

Where does it break — old browsers don't glide

All nine manage the divider through one custom property, --reveal. Registering it with @property as a <percentage> is what lets each auto-preview loop glide smoothly between its low and high point (roughly 20–30% up to 70–80%, depending on the item) — without that registration, a keyframe animation on a custom property can't be interpolated at all, so the browser jumps between values instead of tweening. The good news is that real dragging and clicking write the value straight into an inline style from JS, so a browser that doesn't support this registration still lets a person operate the slider by hand just fine. While building this set, the vertical one (item 05) looked fine at the 480×300 stage but actually overflowed vertically once checked at a 320px-wide viewport, so its card width had to shrink from 200px to 120px — a vertical slider runs out of height on a narrow screen faster than a horizontal one runs out of width. Carrying that drag feel over to fingers on glass is what 9 Mobile Touch Gestures covers. The zip's password is dv4hytn4, and every folder ships both a vanilla and a React version side by side.

Accessibility — what reduced-motion leaves behind

With prefers-reduced-motion: reduce, all nine stop their auto-preview loop (the is-demo animation) and settle at a fixed 50%. Item 04 already lets someone move the whole range with just a keyboard, so turning the animation off costs it nothing. Item 08's zoom follows the same rule and locks its scale to 1 when motion is reduced.

FAQ

Does a before/after slider need JavaScript?

Yes. The divider has to track live input from a click, drag, or key press, which means something has to keep updating --reveal. All nine use between 12 and 34 lines of plain vanilla JS to do it.

How do I swap real photos into this before after slider html?

Replace the background on .ba__after and .ba__before with url(path), or switch the div to an <img> with object-fit: cover. The zip's README spells out both options.

Can I mix vertical and horizontal sliders on one page?

Yes — they only differ in which axis --reveal and clip-path move along, so as long as class names don't collide, both can live on the same page. Lists that wipe a row away sideways share the same horizontal axis — 9 Swipe to Delete List UI Patterns is that family's representative.

The clip-path mechanics are documented on MDN's clip-path page, and pointer capture on MDN's Pointer events page. For a different way to lay out multiple photos, see 9 CSS Image Gallery Grid Effects, and browse more real-world UI in the UI category.

Enter the archive password

The password is inside this article. You will find it as you read.