JavaScriptで作る画像切り替え・プレビュー機能の実装ガイド

javascript icon
JavaScript

こんにちは、とまだです。

ECサイトで商品の色違いをクリックしたら画像が切り替わる機能。

プロフィール画像をアップロードした瞬間にプレビューが表示される機能。

これらの実装、難しそうに見えますよね。

実はJavaScriptの基本的な知識があれば、思っているより簡単に作れるんです。

今回は、画像切り替えやプレビュー機能の実装方法を、実践的なコードとともに解説していきます。

画像切り替え機能が必要な3つの理由

1. ユーザビリティの向上

商品を購入する際、複数の角度や色違いを確認したいですよね。

ページ遷移なしで画像を切り替えられれば、ユーザーのストレスが大幅に減ります。

2. コンバージョン率の改善

商品画像を詳しく確認できることで、購入への不安が解消されます。

実際、複数画像を表示できるECサイトは、単一画像のサイトよりもコンバージョン率が高い傾向にあります。

3. ページパフォーマンスの最適化

画像をクリックするたびにページをリロードするのは非効率です。

JavaScriptで制御することで、必要な画像だけを読み込み、サーバーへの負荷も軽減できます。

基本的な画像切り替えの実装

最初は、サムネイルをクリックしてメイン画像を切り替える基本機能から始めましょう。

HTML構造

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>画像切り替えサンプル</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="gallery-container">
        <img id="mainImage" src="images/product-1.jpg" alt="商品画像">

        <div class="thumbnail-list">
            <img class="thumbnail active" src="images/product-1.jpg" alt="商品1" data-image="images/product-1.jpg">
            <img class="thumbnail" src="images/product-2.jpg" alt="商品2" data-image="images/product-2.jpg">
            <img class="thumbnail" src="images/product-3.jpg" alt="商品3" data-image="images/product-3.jpg">
            <img class="thumbnail" src="images/product-4.jpg" alt="商品4" data-image="images/product-4.jpg">
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

data-image属性を使って、各サムネイルに対応するメイン画像のパスを持たせています。これにより、HTMLとJavaScriptの役割を明確に分離できます。

CSS(スタイリング)

.gallery-container {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

#mainImage {
    width: 100%;
    height: 400px;
    object-fit: contain;
    border: 1px solid #ddd;
    border-radius: 8px;
    margin-bottom: 20px;
}

.thumbnail-list {
    display: flex;
    gap: 10px;
    justify-content: center;
}

.thumbnail {
    width: 80px;
    height: 80px;
    object-fit: cover;
    border: 2px solid transparent;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s ease;
}

.thumbnail:hover {
    transform: scale(1.05);
    border-color: #007bff;
}

.thumbnail.active {
    border-color: #007bff;
    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}

activeクラスで現在選択されているサムネイルを視覚的に示します。ホバー時の拡大効果で、クリッカブルであることを明確にしています。

JavaScript(動作実装)

// DOM要素の取得
const mainImage = document.getElementById('mainImage');
const thumbnails = document.querySelectorAll('.thumbnail');

// 各サムネイルにクリックイベントを設定
thumbnails.forEach(thumbnail => {
    thumbnail.addEventListener('click', function() {
        // メイン画像のsrcを更新
        const newImageSrc = this.getAttribute('data-image');
        mainImage.src = newImageSrc;

        // activeクラスの付け替え
        thumbnails.forEach(thumb => thumb.classList.remove('active'));
        this.classList.add('active');

        // フェードイン効果(オプション)
        mainImage.style.opacity = '0';
        setTimeout(() => {
            mainImage.style.opacity = '1';
        }, 100);
    });
});

// キーボード操作にも対応
let currentIndex = 0;

document.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' && currentIndex > 0) {
        currentIndex--;
        thumbnails[currentIndex].click();
    } else if (e.key === 'ArrowRight' && currentIndex < thumbnails.length - 1) {
        currentIndex++;
        thumbnails[currentIndex].click();
    }
});

このコードでは、data-image属性から画像パスを取得し、メイン画像を更新しています。キーボード操作にも対応することで、アクセシビリティも向上させています。

ファイルアップロード時のプレビュー機能

次は、ユーザーがファイルを選択した瞬間にプレビューを表示する機能を実装します。

プレビュー機能のHTML

<div class="upload-section">
    <h2>プロフィール画像をアップロード</h2>

    <div class="upload-area" id="uploadArea">
        <input type="file" id="fileInput" accept="image/*" style="display: none;">

        <div class="upload-placeholder" id="uploadPlaceholder">
            <svg width="48" height="48" viewBox="0 0 24 24" fill="none">
                <path d="M12 4v16m8-8H4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
            </svg>
            <p>クリックまたはドラッグ&ドロップで画像を選択</p>
            <p class="file-types">対応形式: JPG, PNG, GIF (最大5MB)</p>
        </div>

        <div class="preview-area" id="previewArea" style="display: none;">
            <img id="previewImage" alt="プレビュー">
            <button class="remove-btn" id="removeBtn">×</button>
        </div>
    </div>

    <div class="file-info" id="fileInfo"></div>
</div>

プレビュー機能のCSS

.upload-section {
    max-width: 400px;
    margin: 0 auto;
    padding: 20px;
}

.upload-area {
    position: relative;
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 40px 20px;
    text-align: center;
    transition: all 0.3s ease;
    cursor: pointer;
}

.upload-area:hover {
    border-color: #007bff;
    background-color: #f8f9fa;
}

.upload-area.dragover {
    border-color: #007bff;
    background-color: #e3f2fd;
}

.upload-placeholder svg {
    color: #6c757d;
    margin-bottom: 10px;
}

.file-types {
    font-size: 12px;
    color: #6c757d;
    margin-top: 5px;
}

.preview-area {
    position: relative;
}

.preview-area img {
    max-width: 100%;
    max-height: 300px;
    border-radius: 4px;
}

.remove-btn {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 30px;
    height: 30px;
    background: rgba(0, 0, 0, 0.7);
    color: white;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.file-info {
    margin-top: 15px;
    font-size: 14px;
    color: #6c757d;
}

プレビュー機能のJavaScript

const fileInput = document.getElementById('fileInput');
const uploadArea = document.getElementById('uploadArea');
const uploadPlaceholder = document.getElementById('uploadPlaceholder');
const previewArea = document.getElementById('previewArea');
const previewImage = document.getElementById('previewImage');
const removeBtn = document.getElementById('removeBtn');
const fileInfo = document.getElementById('fileInfo');

// ファイルサイズの上限(5MB)
const MAX_FILE_SIZE = 5 * 1024 * 1024;

// クリックでファイル選択
uploadArea.addEventListener('click', () => {
    fileInput.click();
});

// ファイル選択時の処理
fileInput.addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (file) {
        handleFile(file);
    }
});

// ドラッグ&ドロップの実装
uploadArea.addEventListener('dragover', (e) => {
    e.preventDefault();
    uploadArea.classList.add('dragover');
});

uploadArea.addEventListener('dragleave', () => {
    uploadArea.classList.remove('dragover');
});

uploadArea.addEventListener('drop', (e) => {
    e.preventDefault();
    uploadArea.classList.remove('dragover');

    const file = e.dataTransfer.files[0];
    if (file && file.type.startsWith('image/')) {
        handleFile(file);
    }
});

// ファイル処理関数
function handleFile(file) {
    // ファイルサイズチェック
    if (file.size > MAX_FILE_SIZE) {
        alert('ファイルサイズは5MB以下にしてください。');
        return;
    }

    // ファイルタイプチェック
    if (!file.type.startsWith('image/')) {
        alert('画像ファイルを選択してください。');
        return;
    }

    // FileReaderでプレビュー表示
    const reader = new FileReader();

    reader.onload = (e) => {
        previewImage.src = e.target.result;
        uploadPlaceholder.style.display = 'none';
        previewArea.style.display = 'block';

        // ファイル情報を表示
        const fileSize = (file.size / 1024 / 1024).toFixed(2);
        fileInfo.textContent = `${file.name} (${fileSize}MB)`;
    };

    reader.readAsDataURL(file);
}

// 画像削除機能
removeBtn.addEventListener('click', (e) => {
    e.stopPropagation();

    previewImage.src = '';
    uploadPlaceholder.style.display = 'block';
    previewArea.style.display = 'none';
    fileInfo.textContent = '';
    fileInput.value = '';
});

FileReader APIを使用することで、サーバーにアップロードする前にクライアント側で画像をプレビューできます。ドラッグ&ドロップにも対応し、ユーザビリティを向上させています。

高機能な画像ギャラリーの実装

最後に、プロのWebサイトで使えるような高機能ギャラリーを実装します。

ギャラリーの機能要件

  • 画像の自動切り替え(スライドショー)
  • サムネイルによる画像選択
  • キーボード操作対応
  • 画像の遅延読み込み
  • モバイル対応のタッチ操作

完全なギャラリー実装

class ImageGallery {
    constructor(config) {
        this.container = document.querySelector(config.container);
        this.images = config.images;
        this.currentIndex = 0;
        this.autoPlayInterval = null;
        this.touchStartX = null;

        this.init();
    }

    init() {
        this.render();
        this.attachEvents();

        // 自動再生が有効な場合
        if (this.container.dataset.autoplay === 'true') {
            this.startAutoPlay();
        }
    }

    render() {
        const html = `
            <div class="gallery-main">
                <img class="main-image" src="${this.images[0].src}" alt="${this.images[0].alt}">
                <button class="nav-btn prev" aria-label="前の画像">‹</button>
                <button class="nav-btn next" aria-label="次の画像">›</button>
                <div class="image-counter">${this.currentIndex + 1} / ${this.images.length}</div>
            </div>
            <div class="thumbnail-strip">
                ${this.images.map((img, index) => `
                    <img class="gallery-thumb ${index === 0 ? 'active' : ''}"
                         src="${img.thumb}"
                         alt="${img.alt}"
                         data-index="${index}">
                `).join('')}
            </div>
        `;

        this.container.innerHTML = html;

        // DOM要素をキャッシュ
        this.mainImage = this.container.querySelector('.main-image');
        this.thumbnails = this.container.querySelectorAll('.gallery-thumb');
        this.counter = this.container.querySelector('.image-counter');
        this.prevBtn = this.container.querySelector('.prev');
        this.nextBtn = this.container.querySelector('.next');
    }

    attachEvents() {
        // サムネイルクリック
        this.thumbnails.forEach(thumb => {
            thumb.addEventListener('click', () => {
                const index = parseInt(thumb.dataset.index);
                this.goToImage(index);
            });
        });

        // ナビゲーションボタン
        this.prevBtn.addEventListener('click', () => this.prevImage());
        this.nextBtn.addEventListener('click', () => this.nextImage());

        // キーボード操作
        document.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowLeft') this.prevImage();
            if (e.key === 'ArrowRight') this.nextImage();
        });

        // タッチ操作
        this.mainImage.addEventListener('touchstart', (e) => {
            this.touchStartX = e.touches[0].clientX;
        });

        this.mainImage.addEventListener('touchend', (e) => {
            if (!this.touchStartX) return;

            const touchEndX = e.changedTouches[0].clientX;
            const diff = this.touchStartX - touchEndX;

            if (Math.abs(diff) > 50) {
                if (diff > 0) {
                    this.nextImage();
                } else {
                    this.prevImage();
                }
            }

            this.touchStartX = null;
        });
    }

    goToImage(index) {
        if (index < 0 || index >= this.images.length) return;

        this.currentIndex = index;

        // 画像を更新
        this.mainImage.src = this.images[index].src;
        this.mainImage.alt = this.images[index].alt;

        // サムネイルのアクティブ状態を更新
        this.thumbnails.forEach((thumb, i) => {
            thumb.classList.toggle('active', i === index);
        });

        // カウンターを更新
        this.counter.textContent = `${index + 1} / ${this.images.length}`;

        // 自動再生をリセット
        if (this.autoPlayInterval) {
            this.stopAutoPlay();
            this.startAutoPlay();
        }
    }

    prevImage() {
        const newIndex = this.currentIndex === 0
            ? this.images.length - 1
            : this.currentIndex - 1;
        this.goToImage(newIndex);
    }

    nextImage() {
        const newIndex = this.currentIndex === this.images.length - 1
            ? 0
            : this.currentIndex + 1;
        this.goToImage(newIndex);
    }

    startAutoPlay() {
        this.autoPlayInterval = setInterval(() => {
            this.nextImage();
        }, 3000);
    }

    stopAutoPlay() {
        if (this.autoPlayInterval) {
            clearInterval(this.autoPlayInterval);
            this.autoPlayInterval = null;
        }
    }
}

// 使用例
const gallery = new ImageGallery({
    container: '#productGallery',
    images: [
        { src: 'images/product-1-full.jpg', thumb: 'images/product-1-thumb.jpg', alt: '商品画像1' },
        { src: 'images/product-2-full.jpg', thumb: 'images/product-2-thumb.jpg', alt: '商品画像2' },
        { src: 'images/product-3-full.jpg', thumb: 'images/product-3-thumb.jpg', alt: '商品画像3' },
        { src: 'images/product-4-full.jpg', thumb: 'images/product-4-thumb.jpg', alt: '商品画像4' }
    ]
});

このクラスベースの実装により、複数のギャラリーを同一ページで独立して動作させることができます。

パフォーマンス最適化のテクニック

画像の遅延読み込み

// Intersection Observer APIを使った遅延読み込み
const lazyLoadImages = () => {
    const images = document.querySelectorAll('img[data-src]');

    const imageObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });

    images.forEach(img => imageObserver.observe(img));
};

// DOMContentLoaded後に実行
document.addEventListener('DOMContentLoaded', lazyLoadImages);

画像のプリロード

// 次の画像を事前に読み込む
function preloadNextImage(currentIndex, images) {
    const nextIndex = (currentIndex + 1) % images.length;
    const preloader = new Image();
    preloader.src = images[nextIndex].src;
}

エラーハンドリングとフォールバック

画像が読み込めない場合の対処も重要です。

// 画像読み込みエラー時の処理
function handleImageError(img, fallbackSrc = 'images/no-image.png') {
    img.onerror = function() {
        this.src = fallbackSrc;
        this.onerror = null; // 無限ループを防ぐ
    };
}

// すべての画像要素に適用
document.querySelectorAll('img').forEach(img => {
    handleImageError(img);
});

まとめ

今回は、JavaScriptを使った画像切り替え・プレビュー機能の実装方法を解説しました。

押さえておきたいポイント:

  • 基本的な画像切り替えは、DOM操作とイベントリスナーで実現
  • FileReader APIを使えば、アップロード前のプレビューが可能
  • クラスを使った実装で、再利用可能なコンポーネントが作れる
  • パフォーマンス最適化とエラーハンドリングで、ユーザー体験を向上

これらの技術を組み合わせることで、プロフェッショナルな画像表示機能を実装できます。

ECサイトやポートフォリオサイトなど、画像が重要な役割を果たすWebサイトでは、今回紹介した機能が大いに活用できるはずです。

ぜひ実際にコードを書いて、動かしてみてください。

共有:

著者について

とまだ

とまだ

フルスタックエンジニア

Learning Next の創設者。Ruby on Rails と React を中心に、プログラミング教育に情熱を注いでいます。初心者が楽しく学べる環境作りを目指しています。

著者の詳細を見る →