EN JA ZH ES

ビフォーアフタースライダーの実装 - 画像比較 UI の設計と最適化

· 約 9 分で読めます

画像比較スライダーの用途と設計要件

画像比較スライダー (Before/After Slider) は、2 枚の画像を重ね合わせ、ドラッグ操作で境界線を移動させることで視覚的に差異を確認できる UI コンポーネントです。写真編集のビフォーアフター、画像圧縮の品質比較、Web デザインの新旧比較、医療画像の経時変化など、幅広い場面で活用されています。

設計上の要件:

  • 直感的な操作性: ユーザーが説明なしで操作方法を理解できること。スライダーのハンドルが視覚的に「ドラッグ可能」であることを示す
  • レスポンシブ対応: デスクトップのマウス操作とモバイルのタッチ操作の両方に対応する。画面幅に応じてコンポーネントサイズが適切にスケールする
  • パフォーマンス: ドラッグ中に 60fps を維持する。リペイントを最小限に抑え、GPU アクセラレーションを活用する
  • アクセシビリティ: キーボード操作 (矢印キー) に対応し、スクリーンリーダーで状態を伝達する。ARIA 属性を適切に設定する
  • 画像の同期: 2 枚の画像が完全に同じ位置・サイズで重なること。アスペクト比の異なる画像への対応方針を決める

実装アプローチは大きく 3 つあります: CSS clip-path 方式、overflow: hidden + 幅制御方式、Canvas 描画方式です。それぞれにトレードオフがあり、用途に応じて最適な方式を選択します。本記事では最もパフォーマンスに優れた clip-path 方式を中心に解説します。

HTML 構造とセマンティクス - アクセシブルなマークアップ

画像比較スライダーの HTML 構造は、セマンティクスとアクセシビリティを考慮して設計します。スクリーンリーダーユーザーにも比較の意図が伝わり、キーボードで操作可能な構造にします。

推奨 HTML 構造:

<div class="comparison-slider" role="group" aria-label="画像比較"><div class="comparison-slider__before"><img src="before.webp" alt="処理前の画像" /><span class="comparison-slider__label">Before</span></div><div class="comparison-slider__after"><img src="after.webp" alt="処理後の画像" /><span class="comparison-slider__label">After</span></div><div class="comparison-slider__handle" role="slider" aria-label="比較位置" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" tabindex="0"></div></div>

マークアップのポイント:

  • role="group": コンテナに設定し、内部要素が関連するグループであることを示す
  • role="slider": ハンドル要素に設定し、スライダーコントロールであることをスクリーンリーダーに伝える
  • aria-valuemin/max/now: スライダーの現在位置をパーセンテージで伝達する。JavaScript で動的に更新する
  • tabindex="0": ハンドルをフォーカス可能にし、キーボード操作を受け付ける
  • alt 属性: 各画像に適切な代替テキストを設定し、画像が表示されない環境でも内容を伝える

ラベル表示は CSS の position: absolute で画像の左上/右上に配置し、スライダー操作時に隠れないよう z-index を調整します。ラベルのフォントサイズはコンテナ幅に応じて clamp(0.75rem, 2vw, 1rem) でスケールさせると、レスポンシブ環境で自然に見えます。

CSS 実装 - clip-path によるパフォーマンス最適化

clip-path を使用した実装は、GPU アクセラレーションが効きやすく、ドラッグ中のパフォーマンスに最も優れています。clip-path の変更はレイアウト再計算を発生させず、コンポジットレイヤーの更新のみで済むためです。

基本 CSS:

.comparison-slider { position: relative; overflow: hidden; cursor: col-resize; } .comparison-slider__before, .comparison-slider__after { position: absolute; inset: 0; } .comparison-slider__before img, .comparison-slider__after img { display: block; width: 100%; height: 100%; object-fit: cover; } .comparison-slider__after { clip-path: inset(0 0 0 50%); } .comparison-slider__handle { position: absolute; top: 0; bottom: 0; left: 50%; width: 4px; background: white; transform: translateX(-50%); box-shadow: 0 0 8px rgba(0,0,0,0.3); }

パフォーマンス最適化のポイント:

  • will-change: clip-path: After 要素に設定し、ブラウザにアニメーション対象であることを事前通知する。ただし常時設定するとメモリ消費が増えるため、ドラッグ開始時に追加し終了時に削除する
  • contain: layout: コンテナに設定し、内部の変更が外部レイアウトに影響しないことをブラウザに伝える
  • GPU レイヤー昇格: transform: translateZ(0) でハンドル要素を独立したコンポジットレイヤーに昇格させ、移動時のリペイントを回避する

レスポンシブ対応: コンテナに aspect-ratio: 16/9 (または画像のアスペクト比) を設定し、幅 100% で高さを自動計算させます。画像は object-fit: cover でコンテナに合わせて表示し、アスペクト比が異なる画像でも破綻しない設計にします。

JavaScript 実装 - ドラッグ操作とイベント処理

スライダーのインタラクションを JavaScript で実装します。マウスイベントとタッチイベントの両方に対応し、Pointer Events API を使用することで統一的に処理できます。

コア実装のポイント:

  • Pointer Events API: pointerdownpointermovepointerup を使用する。マウス、タッチ、ペンを 1 つの API で統一的に処理できる。setPointerCapture() でハンドル外に移動してもイベントを受け取り続ける
  • 位置計算: event.clientX - container.getBoundingClientRect().left でコンテナ内の相対位置を算出し、コンテナ幅で割ってパーセンテージ (0-100) に変換する
  • requestAnimationFrame: pointermove イベントは高頻度で発火するため、直接 DOM を更新せず requestAnimationFrame でフレームごとに 1 回だけ更新する
  • 境界制限: パーセンテージを Math.max(0, Math.min(100, percent)) でクランプし、スライダーがコンテナ外に出ないようにする

キーボード操作の実装:

handle.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') updatePosition(currentPercent - 1); if (e.key === 'ArrowRight') updatePosition(currentPercent + 1); if (e.key === 'Home') updatePosition(0); if (e.key === 'End') updatePosition(100); });

DOM 更新関数:

function updatePosition(percent) { percent = Math.max(0, Math.min(100, percent)); afterEl.style.clipPath = `inset(0 0 0 ${percent}%)`; handleEl.style.left = `${percent}%`; handleEl.setAttribute('aria-valuenow', Math.round(percent)); }

タッチデバイスでの注意点: touch-action: none をコンテナに設定し、ブラウザのデフォルトスクロール動作を抑制します。ただし、垂直スクロールは許可したい場合は touch-action: pan-y に変更し、水平方向のみスライダーが反応するようにします。

高度な機能 - アニメーション、遅延読み込み、複数インスタンス

基本実装に加え、ユーザー体験を向上させる高度な機能を実装します。

初期アニメーション (Onboarding):

  • ページ読み込み時にスライダーが左右に揺れるアニメーションを再生し、操作可能であることをユーザーに示す
  • CSS: @keyframes hint { 0%,100% { left: 50% } 25% { left: 35% } 75% { left: 65% } }
  • アニメーションはユーザーが初めてインタラクションした時点で停止する (animation: none を設定)
  • IntersectionObserver でビューポートに入った時にのみアニメーションを開始し、不要な処理を避ける

画像の遅延読み込み:

  • 比較スライダーは通常ページ下部に配置されるため、loading="lazy" で画像を遅延読み込みする
  • 画像読み込み完了前はプレースホルダー (アスペクト比を維持した灰色ボックス) を表示し、CLS (Cumulative Layout Shift) を防止する
  • 両方の画像が読み込み完了してからスライダーを有効化する: Promise.all([img1.decode(), img2.decode()]).then(enableSlider)

複数インスタンスの管理:

  • 1 ページに複数のスライダーを配置する場合、各インスタンスを独立して管理する
  • クラスベースの実装: class ComparisonSlider { constructor(el) { this.container = el; this.init(); } }
  • 初期化: document.querySelectorAll('.comparison-slider').forEach(el => new ComparisonSlider(el))
  • メモリリーク防止: SPA でコンポーネントが破棄される際に destroy() メソッドでイベントリスナーを解除する

縦方向スライダー: clip-path: inset(50% 0 0 0) で上下分割にし、ハンドルを水平に配置することで縦方向の比較も実現できます。data-direction="vertical" 属性で方向を切り替える設計にすると汎用性が高まります。

既存ライブラリの比較と選定基準

自前実装の代わりに、既存のライブラリを活用する選択肢もあります。プロジェクトの要件に応じて、自前実装とライブラリ利用のどちらが適切かを判断します。

主要ライブラリの比較:

  • img-comparison-slider (Web Component): バンドルサイズ 3.5KB (gzip)。Web Component として実装されており、フレームワーク非依存。<img-comparison-slider> タグで宣言的に使用可能。キーボード操作とアクセシビリティに標準対応。最も軽量で推奨
  • TwentyTwenty (jQuery): jQuery 依存のレガシーライブラリ。機能は十分だが、jQuery を使用していないプロジェクトでは不適切。バンドルサイズ 5KB + jQuery 87KB
  • Cocoen: バニラ JavaScript 実装。2KB (gzip) と軽量。タッチ対応済み。ただしアクセシビリティ対応が不十分 (ARIA 属性なし)
  • React Compare Image: React 専用コンポーネント。TypeScript 対応。Props でカスタマイズ可能だが、React プロジェクト以外では使用不可

選定基準:

  • バンドルサイズ重視: img-comparison-slider (3.5KB) または Cocoen (2KB)
  • アクセシビリティ重視: img-comparison-slider (ARIA 完全対応)
  • React プロジェクト: React Compare Image
  • カスタマイズ性重視: 自前実装 (本記事の方式)

自前実装を選ぶべきケース:

  • デザインシステムに完全に統合する必要がある場合
  • 特殊な操作 (ピンチズーム、回転、3 枚以上の比較) が必要な場合
  • パフォーマンス要件が厳しく、不要なコードを一切含めたくない場合
  • 既存ライブラリのアクセシビリティ対応が不十分な場合

いずれの場合も、Core Web Vitals への影響を計測し、LCP (Largest Contentful Paint) を遅延させないよう画像の最適化 (WebP/AVIF 使用、適切なサイズ指定) を併せて実施してください。

関連記事

画像の差分比較手法 - ピクセル単位からセマンティック比較まで

画像の差分を検出・可視化する技術を体系的に解説。ピクセル比較、構造的類似度、知覚的差分など多角的なアプローチを紹介します。

レスポンシブ画像の実装ガイド - srcset, sizes, picture 要素の完全解説

デバイスの画面サイズや解像度に応じて最適な画像を配信するレスポンシブ画像の実装方法を、コード例とともに詳しく解説します。

HTML イメージマップの作り方と代替手段 - クリッカブルマップの実装ガイド

HTML の map 要素と area 要素を使ったイメージマップの実装方法を解説。レスポンシブ対応の課題と、SVG や CSS を使ったモダンな代替手段を具体的なコード例とともに紹介します。

スマホでの画像編集ベストプラクティス - モバイル環境での効率的な写真加工術

スマートフォンでの画像編集を効率化するテクニック。モバイルブラウザでの処理制約、メモリ管理、タッチ UI 設計、PWA での実装方法を実践的に解説します。

Web サイトの画像パフォーマンス監査 - Core Web Vitals 改善の実践ガイド

Web サイトの画像がパフォーマンスに与える影響を監査する方法を解説。LCP 改善、CLS 防止、転送量削減の具体的な手法を紹介します。

画像ギャラリーのパフォーマンス最適化 - 大量画像を高速表示するテクニック

数百枚以上の画像を含むギャラリーページのパフォーマンスを最適化する手法を解説。仮想スクロール、プログレッシブ読み込み、メモリ管理、レイアウト計算の効率化を実践的に紹介します。

関連用語