GODRICH

이미지 비교 슬라이더 UI 9종 — 드래그하면 바로 보이는

이미지 비교 슬라이더는 사진 한 장 위에 다른 사진을 겹쳐 놓고, 경계선을 움직이면 전과 후가 갈라져 보이는 UI입니다. 리모델링·시술 후기 페이지에서 흔히 보지만 클릭 한 번에 바로 켜지는 동작이라 CSS만으로는 못 만들고 JS 몇 줄이 꼭 필요합니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 예쁜 순서가 아니라 이 슬라이더가 실제로 겪는 순서대로 놓았습니다. 클릭 한 번으로 켜보고 → 손으로 계속 끌어보고 → 모바일에서 눌러보고 → 키보드로도 되는지 확인하고 → 방향을 세로로 바꿔보고 → 라벨을 달아보고 → 설명 문구를 붙여보고 → 확대해보고 → 마지막엔 여러 장을 한 페이지에 늘어놓습니다.

01클릭 비교 슬라이더

사진 아무 곳이나 클릭하면 그 자리로 경계선이 바로 옮겨갑니다. 드래그가 아니라 클릭 한 번이라 가장 먼저 시험해보기 좋은 가장 단순한 버전입니다.

clip-path:inset()CSS 커스텀 프로퍼티click→jump
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)) + "%");
});

02드래그 핸들 비교 슬라이더

가운데 원형 핸들을 손가락처럼 끌면 세로선이 실시간으로 따라옵니다. 대부분의 상세페이지가 쓰는 기본형이 이 버전입니다.

pointer 드래그@property 변수handle translateX
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));
});

03터치 스와이프 비교 슬라이더

슬라이더 전체가 터치 타깃이라 핸들의 작은 점을 정확히 짚을 필요가 없습니다. 그립도 48px로 키워 손끝이 굵은 사람도 잘 누를 수 있습니다.

touch-action:nonesetPointerCapture48px 터치 영역
.ba {
  touch-action: none;
  cursor: grab;
}
.ba__grip {
  width: $sp-12;   // 48px — 손끝 굵기를 고려한 최소 터치 크기
  height: $sp-12;
  border-radius: $r-pill;
}

04키보드 접근성 비교 슬라이더

Tab으로 초점을 옮긴 뒤 방향키를 누르면 5%씩, Home·End를 누르면 양 끝으로 옮겨갑니다. 마우스 없이도 전후를 비교할 수 있습니다.

role=slideraria-valuenow방향키 5% 이동
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(); }
});

05세로 방향 비교 슬라이더

경계선이 좌우 대신 위아래로 움직여, 세로로 긴 사진을 비교할 때 원본을 회전하지 않아도 됩니다.

clip-path 세로축--reveal 변수vh 무대
.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 라벨 배지

좌우 위쪽에 고정된 알약 배지가 경계선 위치에 맞춰 opacity로 옅어지고 짙어져, 지금 보는 쪽이 전인지 후인지 헷갈리지 않게 합니다.

calc() opacity$r-pill 배지position:absolute 라벨
function set(pct) {
  pct = Math.max(0, Math.min(100, pct));
  // 위치(%)와 0~1 사이 숫자를 각각 등록해서 opacity 는 숫자 쪽을 쓴다
  el.style.setProperty("--reveal", pct + "%");
  el.style.setProperty("--reveal-num", (pct / 100).toFixed(3));
}

07캡션 오버레이 비교 슬라이더

하단에 반투명 그러데이션 띠가 깔리고 "3주 경과" 같은 캡션 문구가 함께 떠, 무엇을 비교하는 사진인지 말로도 설명해 줍니다.

linear-gradient 오버레이clip-path: insetcaption 텍스트
.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;
}

08줌 동기화 비교 슬라이더

슬라이더 위에서 휠을 굴리면 전·후 두 장이 같은 배율·같은 중심점으로 함께 확대돼, 세부를 확대해서 봐도 비교가 어긋나지 않습니다.

transform:scale() 동기화wheel 줌transform-origin 고정
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 });

09여러 장 비교 캐러셀

scroll-snap 캐러셀 안에 전후 슬라이더 세 쌍을 나란히 두고, IntersectionObserver로 지금 화면 중앙에 온 슬라이드만 드래그가 켜집니다.

scroll-snap-type:xIntersectionObserver슬라이더 다중 관리
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); });

어디서 깨지는가 — 오래된 브라우저는 부드럽게 안 밀린다

아홉 개 전부 --reveal이라는 커스텀 프로퍼티 하나로 경계선 위치를 관리합니다. 이 값을 @property<percentage> 타입으로 등록해야 자동 미리보기의 경계선이 설정한 구간 안에서 부드럽게 밀립니다 — 등록 없이 키프레임에 커스텀 프로퍼티를 쓰면 브라우저가 값을 보간하지 못해 뚝뚝 끊기며 점프합니다. 다행히 실제 드래그·클릭 조작은 JS가 인라인 스타일로 값을 직접 써 넣기 때문에 이 등록을 지원하지 않는 아주 오래된 브라우저에서도 사용자가 손으로 조작하는 데는 문제가 없습니다. 만드는 동안 세로형(05번) 하나는 480×300 무대에선 멀쩡했는데 320px 폭 확인에서 실제로 높이가 넘쳐 카드 폭을 200px에서 120px로 줄여야 했습니다 — 가로 슬라이더보다 세로 슬라이더가 좁은 화면에서 더 쉽게 넘칩니다. 아홉 파일의 압축 비밀번호는 dv4hytn4이고, zip을 열면 vanilla·react 버전이 항목마다 같이 들어 있습니다.

접근성 — reduced-motion에서 아홉 개가 남기는 것

prefers-reduced-motion: reduce를 걸면 아홉 개 모두 자동 미리보기 애니메이션(is-demo 루프)이 멈추고 경계선이 50% 고정값에 머뭅니다. 04번은 원래도 키보드만으로 전 구간을 조작할 수 있어 애니메이션이 꺼져도 기능이 줄지 않습니다. 08번의 줌도 같은 규칙으로 정지 상태에선 배율이 1로 고정됩니다.

FAQ

이미지 비교 슬라이더는 꼭 JS가 있어야 하나요?

네. 경계선이 클릭·드래그·키보드 같은 실시간 입력을 따라가야 해서 --reveal 값을 계속 갱신하는 JS 코드가 필요합니다. 아홉 개 전부 12~34줄 사이의 짧은 바닐라 JS만 씁니다.

사진은 어떻게 넣나요?

.ba__after·.ba__beforebackground 자리에 url(경로)로 실제 사진을 넣거나, <img> 태그로 바꿔 object-fit: cover를 주면 됩니다. zip 안 README에 넣는 법이 적혀 있습니다.

세로형과 가로형을 한 페이지에 같이 써도 되나요?

됩니다. --reveal 커스텀 프로퍼티와 clip-path 축만 다를 뿐 서로 클래스 이름이 겹치지 않게 스코프만 나누면 같은 페이지 안에서 함께 씁니다.

전후 비교의 clip-path 원리는 MDN clip-path 문서에, 포인터 캡처는 MDN Pointer events 문서에 정리돼 있습니다. 이미지를 겹쳐 보여주는 다른 방식은 이미지 갤러리 CSS 9종에서, 실전 UI 모음은 실전 UI 카테고리에서 더 볼 수 있습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.