GODRICH

ビフォーアフター スライダー UI 9選 — ドラッグで比較

ビフォーアフター スライダーは2枚の写真を重ね、動かせる境界線で前後を並べて見せるUIです。境界線はクリックやドラッグにリアルタイムで反応する必要があるため、CSSだけでは作れずJSが数行必要です。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つは見た目の順番ではなく、このスライダーが実際に使われる順番で並べました。まずクリックで動かしてみて → 指でずっと引っぱってみて → スマホで押してみて → キーボードだけで操作できるか確認して → 向きを縦に変えてみて → ラベルを付けてみて → キャプションを添えてみて → 拡大してみて → 最後に複数組を1ページに並べます。

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ズーム同期比較スライダー

スライダー上でホイールを回すと前後2枚が同じ倍率・同じ中心点で一緒に拡大され、細部を拡大しても比較がずれません。

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カルーセルの中に前後スライダー3組を並べ、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); });

どこで壊れるか — 古いブラウザは滑らかに動かない

9つとも --reveal というカスタムプロパティ一つで境界線の位置を管理しています。この値を @property<percentage> 型として登録して初めて、自動プレビューの境界線が20%から80%まで滑らかに動きます — 登録しないままキーフレームでカスタムプロパティを動かすと、ブラウザが値を補間できずカクカクと飛びます。幸い実際のドラッグ・クリック操作はJSがインラインスタイルに値を直接書き込むため、この登録に対応しない古いブラウザでも手での操作自体は問題なく動きます。制作中、縦型(05番)は480×300の舞台では問題なかったのに、320px幅で確認すると実際に高さがはみ出し、カード幅を200pxから120pxまで縮める必要がありました — 横型より縦型のほうが、狭い画面で先にあふれます。この320px確認は9項目すべてに毎回行っていて、はみ出しがあれば数値でそのまま分かるので見た目だけで判断しません。9ファイルの圧縮パスワードはdv4hytn4で、zipにはどの項目もvanilla版とReact版が並んで入っています。

アクセシビリティ — reduced-motionで9つが残すもの

prefers-reduced-motion: reduce を指定すると、9つとも自動プレビューのループ(is-demoアニメーション)が止まり、境界線は50%で固定されます。04番はもともとキーボードだけで全域を操作できるため、アニメーションを止めても機能は減りません。08番のズームも同じ規則で、動きを抑えると倍率が1に固定されます。

FAQ

ビフォーアフター スライダーにはJavaScriptが必須ですか?

はい。境界線はクリック・ドラッグ・キー入力といったリアルタイムの入力に追従する必要があり、--reveal を更新し続けるコードが要ります。9つとも12〜34行の短いバニラJSだけで書いています。

写真はどう差し替えますか?

.ba__after.ba__beforebackgroundurl(パス) を指定するか、<img> タグに変えて object-fit: cover を指定します。zip内のREADMEに両方の方法を書いています。

縦型と横型を同じページに混在させても大丈夫ですか?

大丈夫です。--revealclip-path の軸が違うだけなので、クラス名さえ衝突しなければ同じページに共存できます。

clip-pathの仕組みはMDNのclip-pathページに、ポインターキャプチャはMDNのPointer eventsページにまとまっています。複数の写真を並べる別の方法はCSS フォト ギャラリー 9選で、実用UIはUIカテゴリーでさらに見られます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。