Membuat Carousel Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Membuat Carousel Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Membuat Carousel Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Novian Hidayat
2025-05-11

Belajar membuat carousel gambar (slider) yang fungsional dan responsif dari nol. Panduan ini akan memandu Anda melalui struktur HTML, styling CSS, dan logika JavaScript murni untuk kontrol navigasi dan efek transisi.

Membuat Carousel Kustom Modern & Responsif dari Awal dengan HTML, CSS, dan Vanilla JavaScript

Selamat datang di tutorial pembuatan carousel (slider) kustom! Carousel adalah komponen UI yang populer untuk menampilkan serangkaian konten (gambar, teks, kartu) secara bergantian. Daripada menggunakan library eksternal, membuat carousel sendiri adalah cara terbaik untuk memahami cara kerjanya dan mengasah keterampilan front-end Anda.

  • Pembelajaran Mendalam: Anda akan memahami mekanika di baliknya.
  • Kustomisasi Penuh: Anda memiliki kontrol penuh atas tampilan dan perilaku.
  • Ringan: Tidak ada kode berlebih dari library yang tidak Anda butuhkan.

Apa yang Akan Kita Buat?

Kita akan membuat carousel gambar sederhana dengan fitur berikut:

  1. Struktur HTML yang jelas.
  2. Slide yang bisa berisi gambar atau konten lain.
  3. Tombol Navigasi “Previous” (Sebelumnya) dan “Next” (Berikutnya).
  4. Indikator Titik (Dots) untuk menunjukkan slide aktif dan navigasi.
  5. Transisi Geser (Slide) yang mulus menggunakan CSS.
  6. Logika JavaScript murni (Vanilla JS) untuk mengontrol fungsionalitas.
  7. Desain Responsif.

Prasyarat

  • Text Editor: VS Code, Sublime Text, dll.
  • Web Browser: Chrome, Firefox, dll.
  • Pemahaman Dasar HTML dan CSS.
  • Sedikit Pengetahuan JavaScript: Variabel, fungsi, event listener, manipulasi DOM.
  • Beberapa Gambar: Siapkan 3-5 gambar untuk slide carousel Anda.

Mari kita mulai!


Langkah 1: Persiapan Proyek dan Struktur Folder

  1. Buat folder utama proyek, misalnya proyek-carousel-kustom.
  2. Di dalamnya, buat:
    • index.html
    • style.css
    • script.js
    • Folder images (tempatkan gambar-gambar Anda di sini, misalnya slide1.jpg, slide2.jpg, slide3.jpg).

Struktur dasar:

proyek-carousel-kustom/
├── index.html
├── style.css
├── script.js
└── images/
    ├── slide1.jpg
    ├── slide2.jpg
    └── slide3.jpg
    └── (gambar lain...)

Langkah 2: Struktur Dasar HTML (index.html)

Buka index.html dan buat kerangka dasar carousel.

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Carousel Kustom - Vanilla JS</title>
    <link rel="stylesheet" href="style.css">
    <!-- Font Awesome untuk ikon panah (opsional, bisa diganti karakter UTF atau SVG) -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>

    <div class="carousel-container">
        <div class="carousel-track-container">
            <ul class="carousel-track">
                <li class="carousel-slide current-slide">
                    <img src="images/slide1.jpg" alt="Gambar Slide 1">
                    <!-- <div class="carousel-caption">Judul Slide 1</div> -->
                </li>
                <li class="carousel-slide">
                    <img src="images/slide2.jpg" alt="Gambar Slide 2">
                    <!-- <div class="carousel-caption">Judul Slide 2</div> -->
                </li>
                <li class="carousel-slide">
                    <img src="images/slide3.jpg" alt="Gambar Slide 3">
                    <!-- <div class="carousel-caption">Judul Slide 3</div> -->
                </li>
                <!-- Tambahkan lebih banyak slide jika perlu -->
            </ul>
        </div>

        <button class="carousel-button carousel-button--left">
            <i class="fas fa-chevron-left"></i>
        </button>
        <button class="carousel-button carousel-button--right">
            <i class="fas fa-chevron-right"></i>
        </button>

        <div class="carousel-nav">
            <button class="carousel-indicator current-slide-indicator"></button>
            <button class="carousel-indicator"></button>
            <button class="carousel-indicator"></button>
            <!-- Jumlah indikator harus sama dengan jumlah slide -->
        </div>
    </div>

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

Penjelasan HTML:

  • .carousel-container: Wrapper utama carousel. Ini akan memiliki overflow: hidden.
  • .carousel-track-container: Kontainer untuk track, membantu dalam beberapa teknik styling. (Dalam kasus sederhana, ini mungkin tidak selalu diperlukan, tapi bisa berguna untuk layout yang lebih kompleks atau efek). Untuk kasus ini, kita bisa menyederhanakannya jika track langsung di dalam container. Kita akan buat tanpa carousel-track-container agar lebih sederhana.
  • .carousel-track: Ini adalah elemen ul yang akan bergerak secara horizontal. Lebarnya akan jauh lebih besar dari kontainernya.
  • .carousel-slide: Setiap li adalah satu slide. current-slide menandai slide yang aktif.
  • .carousel-button: Tombol “Previous” dan “Next”.
  • .carousel-nav: Kontainer untuk indikator titik (dots).
  • .carousel-indicator: Setiap tombol titik. current-slide-indicator menandai titik untuk slide yang aktif.

Penyederhanaan Struktur (Opsional): Kita bisa menghilangkan .carousel-track-container jika tidak ada kebutuhan khusus. Mari kita lanjutkan dengan struktur di atas dulu, dan jika tidak diperlukan, kita bisa hapus di CSS nanti. Untuk tutorial ini, kita akan coba tanpa .carousel-track-container agar lebih fokus pada .carousel-track itu sendiri.

Struktur HTML yang disederhanakan (digunakan untuk tutorial ini):

<!-- index.html (bagian carousel) -->
<div class="carousel-container">
    <ul class="carousel-track">
        <li class="carousel-slide current-slide">
            <img src="images/slide1.jpg" alt="Gambar Slide 1">
        </li>
        <li class="carousel-slide">
            <img src="images/slide2.jpg" alt="Gambar Slide 2">
        </li>
        <li class="carousel-slide">
            <img src="images/slide3.jpg" alt="Gambar Slide 3">
        </li>
    </ul>

    <button class="carousel-button carousel-button--left">
        <i class="fas fa-chevron-left"></i>
    </button>
    <button class="carousel-button carousel-button--right">
        <i class="fas fa-chevron-right"></i>
    </button>

    <div class="carousel-nav">
        <!-- Indikator akan dibuat dinamis dengan JS atau di-hardcode sejumlah slide -->
    </div>
</div>

Kita akan membuat indikator secara dinamis dengan JS nanti, jadi biarkan .carousel-nav kosong dulu.


Langkah 3: Styling Dasar CSS (style.css)

Buka style.css. Kita akan mengatur agar slide berjajar horizontal dan hanya satu yang terlihat.

/* style.css */
body {
    margin: 0;
    font-family: Arial, sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #f0f0f0;
}

.carousel-container {
    position: relative; /* Kunci untuk positioning absolut tombol dan nav */
    width: 80%; /* Atau ukuran spesifik: 800px */
    max-width: 900px;
    height: 500px; /* Sesuaikan dengan rasio gambar Anda */
    overflow: hidden; /* SANGAT PENTING: menyembunyikan slide lain */
    border-radius: 10px;
    box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}

.carousel-track {
    list-style: none;
    padding: 0;
    margin: 0;
    height: 100%;
    display: flex; /* Membuat semua slide berjajar horizontal */
    transition: transform 0.5s ease-in-out; /* Animasi geser */
    /* Lebar track akan diatur oleh JS atau bisa 300% jika ada 3 slide, 100% per slide */
}

.carousel-slide {
    min-width: 100%; /* Setiap slide mengambil lebar penuh dari container */
    height: 100%;
    /* flex-basis: 100%; juga bisa digunakan */
    /* display: flex;
    align-items: center;
    justify-content: center; */
}

.carousel-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover; /* Memastikan gambar mengisi slide tanpa distorsi */
    display: block; /* Menghilangkan spasi ekstra di bawah gambar */
}

/* Caption (Opsional) */
.carousel-caption {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background-color: rgba(0,0,0,0.5);
    color: white;
    padding: 10px 20px;
    border-radius: 5px;
    font-size: 1.2rem;
}

Penjelasan CSS Awal:

  • .carousel-container: position: relative agar tombol navigasi dan dots bisa diposisikan absolut terhadapnya. overflow: hidden adalah properti ajaib yang membuat hanya satu slide terlihat.
  • .carousel-track: display: flex membuat li (slide) berjajar horizontal. transition akan menganimasikan perubahan properti transform (yang akan kita gunakan untuk menggeser track).
  • .carousel-slide: min-width: 100% memastikan setiap slide mengambil lebar penuh dari .carousel-container.
  • .carousel-slide img: object-fit: cover membuat gambar mengisi area slide dengan baik.

Jika Anda buka index.html sekarang, Anda akan melihat slide pertama, dan jika Anda inspect element pada .carousel-track lalu secara manual menambahkan transform: translateX(-100%);, Anda akan melihat slide kedua. Ini dasar dari cara kerjanya.


Langkah 4: Styling Tombol Navigasi dan Indikator Titik

/* Lanjutan style.css */

.carousel-button {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background-color: rgba(0,0,0,0.4);
    color: white;
    border: none;
    padding: 15px; /* Lebih besar agar mudah diklik */
    cursor: pointer;
    z-index: 10; /* Di atas slide */
    border-radius: 50%; /* Membuat tombol bulat */
    width: 50px;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    transition: background-color 0.3s ease;
}

.carousel-button:hover {
    background-color: rgba(0,0,0,0.7);
}

.carousel-button--left {
    left: 20px;
}

.carousel-button--right {
    right: 20px;
}

/* Tombol disabled (akan ditambahkan dengan JS) */
.carousel-button.is-hidden {
    display: none; /* Atau opacity: 0.5; pointer-events: none; */
}


.carousel-nav {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    z-index: 10;
}

.carousel-indicator {
    background-color: rgba(255,255,255,0.5); /* Titik transparan */
    border: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    margin: 0 6px;
    cursor: pointer;
    padding: 0; /* Hapus padding default tombol */
    transition: background-color 0.3s ease, transform 0.3s ease;
}

.carousel-indicator:hover {
    background-color: rgba(255,255,255,0.8);
    transform: scale(1.1);
}

.carousel-indicator.current-slide-indicator {
    background-color: white; /* Titik aktif lebih jelas */
    transform: scale(1.2);
}

Langkah 5: JavaScript - Seleksi Elemen dan Inisialisasi State

Buka script.js. Kita akan mulai dengan mengambil referensi ke elemen HTML yang kita butuhkan.

// script.js
document.addEventListener('DOMContentLoaded', () => {
    const track = document.querySelector('.carousel-track');
    const slides = Array.from(track.children); // Mengubah HTMLCollection menjadi Array
    const nextButton = document.querySelector('.carousel-button--right');
    const prevButton = document.querySelector('.carousel-button--left');
    const dotsNav = document.querySelector('.carousel-nav');

    // Jika ingin membuat dots secara dinamis
    slides.forEach((slide, index) => {
        const dot = document.createElement('button');
        dot.classList.add('carousel-indicator');
        if (index === 0) {
            dot.classList.add('current-slide-indicator');
        }
        dotsNav.appendChild(dot);
    });
    const dots = Array.from(dotsNav.children);

    const slideWidth = slides[0].getBoundingClientRect().width; // Lebar satu slide
    let currentIndex = 0; // Indeks slide yang sedang aktif

    // Atur posisi awal slide (tidak selalu diperlukan jika CSS sudah benar)
    // slides.forEach((slide, index) => {
    //     slide.style.left = slideWidth * index + 'px'; // Ini untuk positioning absolut, kita pakai flexbox jadi tidak perlu
    // });

    // Fungsi untuk memindahkan slide
    const moveToSlide = (targetIndex) => {
        if (!track || !slides[targetIndex]) return; // Guard clause

        track.style.transform = `translateX(-${slideWidth * targetIndex}px)`;
        
        // Update kelas 'current-slide' pada slide
        slides[currentIndex].classList.remove('current-slide');
        slides[targetIndex].classList.add('current-slide');
        
        // Update kelas 'current-slide-indicator' pada dot
        if (dots.length > 0) {
            dots[currentIndex].classList.remove('current-slide-indicator');
            dots[targetIndex].classList.add('current-slide-indicator');
        }

        currentIndex = targetIndex;
        updateNavButtons();
    };

    // Fungsi untuk update status tombol prev/next
    const updateNavButtons = () => {
        if (!prevButton || !nextButton) return;
        if (currentIndex === 0) {
            prevButton.classList.add('is-hidden'); // Sembunyikan tombol prev di slide pertama
            nextButton.classList.remove('is-hidden');
        } else if (currentIndex === slides.length - 1) {
            nextButton.classList.add('is-hidden'); // Sembunyikan tombol next di slide terakhir
            prevButton.classList.remove('is-hidden');
        } else {
            prevButton.classList.remove('is-hidden');
            nextButton.classList.remove('is-hidden');
        }
    };

    // Panggil updateNavButtons saat pertama kali load
    updateNavButtons();


    // Event Listener untuk Tombol Next
    if (nextButton) {
        nextButton.addEventListener('click', e => {
            if (currentIndex < slides.length - 1) {
                moveToSlide(currentIndex + 1);
            }
        });
    }

    // Event Listener untuk Tombol Prev
    if (prevButton) {
        prevButton.addEventListener('click', e => {
            if (currentIndex > 0) {
                moveToSlide(currentIndex - 1);
            }
        });
    }

    // Event Listener untuk Indikator Titik
    if (dotsNav) {
        dotsNav.addEventListener('click', e => {
            const targetDot = e.target.closest('button.carousel-indicator');
            if (!targetDot) return;

            const targetIndex = dots.findIndex(dot => dot === targetDot);
            if (targetIndex !== -1) {
                moveToSlide(targetIndex);
            }
        });
    }
    
    // (Opsional) Responsif: Update slideWidth jika ukuran window berubah
    // Ini penting jika lebar carousel Anda responsif (misal, width: 80%)
    let resizeTimeout;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimeout);
        resizeTimeout = setTimeout(() => {
            const newSlideWidth = slides[0].getBoundingClientRect().width;
            // Hanya update jika lebar benar-benar berubah untuk menghindari kalkulasi berlebih
            if (newSlideWidth !== slideWidth) {
                 // Perlu cara untuk mendapatkan slideWidth yang baru dan mengaplikasikannya
                 // Untuk carousel flexbox sederhana ini, width:100% pada slide sudah cukup.
                 // Yang perlu di-recalculate adalah `transform` jika slideWidth berubah.
                 // Jadi, panggil moveToSlide lagi dengan index saat ini agar `translateX` dihitung ulang.
                 // Tapi, karena slideWidth didapat dari `getBoundingClientRect`, ia sudah akan otomatis benar
                 // saat dipanggil di `moveToSlide`.
                 // Poin penting adalah: `getBoundingClientRect().width` akan selalu memberikan lebar aktual.
                 // Jadi, kita hanya perlu memastikan `moveToSlide` menggunakan nilai terbaru.
                 // Untuk kasus ini, karena `slideWidth` hanya di-assign sekali, kita perlu meng-update-nya.
                 // Atau, lebih baik, ambil `slideWidth` langsung di dalam `moveToSlide`.

                 // Versi lebih baik: Dapatkan slideWidth dinamis di dalam moveToSlide
                 // Maka, hapus `const slideWidth = slides[0].getBoundingClientRect().width;` di atas
                 // dan letakkan di dalam `moveToSlide`
                 // Contoh di bawah akan pakai `slideWidth` yang di-scope di luar fungsi dulu.
                 // Jika ada masalah responsivitas, pindahkan kalkulasi slideWidth ke dalam moveToSlide.

                 // Cara 1: Update slideWidth global dan panggil moveToSlide
                 // slideWidth = newSlideWidth; // Update variabel globalnya
                 // moveToSlide(currentIndex);

                 // Cara 2 (lebih bersih): Hitung slideWidth di dalam moveToSlide
                 // (Ini akan jadi pembaruan di langkah berikutnya jika diperlukan)

                 // Untuk sekarang, kita panggil ulang moveToSlide dengan currentIndex
                 // Ini akan menggunakan slideWidth yang lama, tapi untuk resize biasanya cukup cepat
                 // sehingga nilai baru sudah terpakai di `getBoundingClientRect` berikutnya.
                 // Jika tidak, kita perlu memodifikasi `moveToSlide`.

                 // *Revisi untuk Responsivitas*
                 // Kita modifikasi moveToSlide agar selalu menggunakan lebar slide saat ini.
                 // Hapus deklarasi `const slideWidth = slides[0].getBoundingClientRect().width;` di atas
                 // dan modifikasi `moveToSlide`

                 // Untuk sekarang, panggil ulang moveToSlide dengan currentIndex
                 moveToSlide(currentIndex); // Panggil ulang untuk re-calculate transform
        }, 250); // Debounce
    });
});

Revisi moveToSlide untuk Responsivitas yang Lebih Baik: Hapus const slideWidth = slides[0].getBoundingClientRect().width; dari bagian atas script.js. Kemudian, modifikasi fungsi moveToSlide menjadi seperti ini:

// script.js (modifikasi fungsi moveToSlide)
    // ... (kode di atasnya tetap sama)

    // Fungsi untuk memindahkan slide
    const moveToSlide = (targetIndex) => {
        if (!track || !slides[targetIndex]) return;

        const currentSlideWidth = slides[targetIndex].getBoundingClientRect().width; // Dapatkan lebar saat ini
        track.style.transform = `translateX(-${currentSlideWidth * targetIndex}px)`;
        
        // Update kelas 'current-slide' pada slide
        // Pastikan currentIndex di-update sebelum menghapus kelas dari slide lama
        if (slides[currentIndex]) { // Cek jika currentIndex valid
            slides[currentIndex].classList.remove('current-slide');
        }
        slides[targetIndex].classList.add('current-slide');
        
        // Update kelas 'current-slide-indicator' pada dot
        if (dots.length > 0 && dots[currentIndex] && dots[targetIndex]) { // Cek dots
            dots[currentIndex].classList.remove('current-slide-indicator');
            dots[targetIndex].classList.add('current-slide-indicator');
        }

        currentIndex = targetIndex; // Update currentIndex SETELAH semua operasi yang membutuhkannya
        updateNavButtons();
    };

    // ... (sisa kode tetap sama, termasuk event listener resize yang memanggil moveToSlide(currentIndex))

Penjelasan JavaScript:

  1. Seleksi Elemen: Mengambil semua elemen DOM yang kita butuhkan. Array.from(track.children) mengubah HTMLCollection menjadi array agar kita bisa menggunakan metode array seperti forEach atau findIndex.
  2. Pembuatan Dots Dinamis: Looping melalui slides untuk membuat tombol indikator sejumlah slide.
  3. slideWidth: (Sebelum revisi) Mendapatkan lebar satu slide. Ini penting untuk kalkulasi translateX. (Setelah revisi) Dihitung dinamis di dalam moveToSlide.
  4. currentIndex: Menyimpan indeks slide yang sedang aktif (dimulai dari 0).
  5. moveToSlide(targetIndex): Fungsi inti.
    • Menghitung seberapa jauh .carousel-track harus digeser ke kiri (translateX).
    • Mengupdate kelas current-slide pada elemen slide.
    • Mengupdate kelas current-slide-indicator pada elemen dot.
    • Memperbarui currentIndex.
    • Memanggil updateNavButtons() untuk menyembunyikan/menampilkan tombol prev/next.
  6. updateNavButtons(): Menyembunyikan tombol “prev” jika di slide pertama, dan tombol “next” jika di slide terakhir.
  7. Event Listeners:
    • Untuk tombol “Next”: Pindah ke currentIndex + 1.
    • Untuk tombol “Prev”: Pindah ke currentIndex - 1.
    • Untuk Dots: Mendapatkan indeks dot yang diklik, lalu pindah ke slide dengan indeks tersebut.
  8. Resize Listener (Opsional tapi Penting untuk Responsif):
    • Saat ukuran window berubah, lebar slide (slideWidth) bisa berubah jika carousel Anda menggunakan unit persentase.
    • getBoundingClientRect().width akan selalu memberikan lebar aktual elemen.
    • Dengan memanggil moveToSlide(currentIndex) lagi setelah resize (dengan debounce), posisi translateX akan dihitung ulang menggunakan slideWidth yang baru, menjaga carousel tetap sinkron. Revisi terakhir memastikan currentSlideWidth selalu yang terbaru.

Simpan semua file (index.html, style.css, script.js). Buka index.html di browser Anda. Anda seharusnya sekarang memiliki carousel yang berfungsi!

  • Tombol Previous/Next seharusnya bekerja.
  • Indikator titik seharusnya bekerja dan menunjukkan slide aktif.
  • Transisi geser seharusnya mulus.
  • Tombol prev/next seharusnya disembunyikan di slide pertama/terakhir.
  • Coba resize window browser Anda, carousel seharusnya menyesuaikan diri.

Langkah 7: (Opsional) Auto-Play

Jika Anda ingin carousel berputar otomatis:

Tambahkan ini di dalam script.js Anda, di dalam DOMContentLoaded:

// script.js (lanjutan, di dalam DOMContentLoaded)
    // ... (semua kode sebelumnya) ...

    // --- Fitur Auto-Play (Opsional) ---
    let autoPlayInterval = null;
    const autoPlayDelay = 5000; // 5 detik

    function startAutoPlay() {
        if (autoPlayInterval) clearInterval(autoPlayInterval); // Hapus interval lama jika ada
        autoPlayInterval = setInterval(() => {
            let nextIndex = currentIndex + 1;
            if

Tutorial Terkait