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

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

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.

Penulis: Novian Hidayat
Tanggal: 11 Mei 2025

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() {
        // Mencegah beberapa interval berjalan bersamaan
        if (autoPlayInterval) clearInterval(autoPlayInterval); 
        
        autoPlayInterval = setInterval(() => {
            // Menggunakan operator modulo untuk looping kembali ke awal
            const nextIndex = (currentIndex + 1) % slides.length;
            moveToSlide(nextIndex);
        }, autoPlayDelay);
    }

    function stopAutoPlay() {
        clearInterval(autoPlayInterval);
    }

    // Mulai auto-play saat halaman dimuat
    startAutoPlay();

    // Jeda auto-play saat mouse berada di atas carousel (user-friendly)
    const carouselContainer = document.querySelector('.carousel-container');
    if (carouselContainer) {
        carouselContainer.addEventListener('mouseenter', stopAutoPlay);
        carouselContainer.addEventListener('mouseleave', startAutoPlay);
    }

Sekarang, kita perlu sedikit memodifikasi event listener yang ada agar auto-play di-reset setiap kali pengguna berinteraksi secara manual. Ini memberikan pengalaman yang lebih baik, karena timer akan diatur ulang setelah pengguna mengklik.

Modifikasi Event Listener yang Sudah Ada:

Ganti blok event listener yang lama dengan yang baru di bawah ini. Kita hanya menambahkan panggilan ke stopAutoPlay() dan startAutoPlay().

// script.js (modifikasi event listener)

    // Event Listener untuk Tombol Next
    if (nextButton) {
        nextButton.addEventListener('click', e => {
            if (currentIndex < slides.length - 1) {
                moveToSlide(currentIndex + 1);
            } else {
                // Opsional: jika ingin looping saat klik tombol next di slide terakhir
                // moveToSlide(0); 
            }
            // Reset auto-play timer setelah interaksi manual
            // startAutoPlay(); // Jika ingin auto-play lanjut setelah klik
        });
    }

    // Event Listener untuk Tombol Prev
    if (prevButton) {
        prevButton.addEventListener('click', e => {
            if (currentIndex > 0) {
                moveToSlide(currentIndex - 1);
            } else {
                // Opsional: jika ingin looping saat klik tombol prev di slide pertama
                // moveToSlide(slides.length - 1);
            }
            // Reset auto-play timer setelah interaksi manual
            // startAutoPlay(); // Jika ingin auto-play lanjut setelah klik
        });
    }

    // 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);
                // Reset auto-play timer setelah interaksi manual
                // startAutoPlay(); // Jika ingin auto-play lanjut setelah klik
            }
        });
    }

Catatan: Saya menonaktifkan startAutoPlay() di dalam klik dengan komentar. Perilaku umum adalah jika pengguna berinteraksi manual, auto-play berhenti. Jika Anda ingin auto-play dilanjutkan setelah klik, hapus tanda komentar //.


Langkah 8: Penyempurnaan Lanjutan (Opsional)

Carousel Anda sudah berfungsi penuh. Namun, ada banyak cara untuk membuatnya lebih canggih.

  • Looping Tak Terbatas (Infinite Loop): Saat ini, carousel berhenti di slide pertama dan terakhir. Untuk membuat loop tak terbatas, Anda perlu teknik yang lebih canggih, biasanya dengan menduplikasi slide pertama dan terakhir, lalu memindahkannya secara instan tanpa transisi saat mencapai ujung “palsu” tersebut.
  • Gestur Geser (Swipe/Drag) untuk Mobile: Untuk pengalaman mobile yang lebih baik, tambahkan event listener untuk touchstart, touchmove, dan touchend untuk memungkinkan pengguna menggeser slide dengan jari.
  • Aksesibilitas (WAI-ARIA): Tingkatkan aksesibilitas untuk pengguna pembaca layar dengan menambahkan atribut ARIA seperti aria-live pada track, aria-controls pada tombol, dan aria-label untuk navigasi yang lebih jelas.
  • Transisi Fade: Jika Anda tidak suka efek geser, Anda bisa mengubahnya menjadi efek fade. Ini memerlukan perubahan CSS (menggunakan opacity dan position: absolute pada slide) dan sedikit logika JavaScript yang berbeda.
  • Lazy Loading Gambar: Jika carousel Anda memiliki banyak gambar beresolusi tinggi, memuat semuanya sekaligus dapat memperlambat situs. Terapkan lazy loading agar gambar hanya dimuat saat slide akan ditampilkan.

Penutup

Selamat! Anda telah berhasil membangun carousel kustom dari awal hanya dengan HTML, CSS, dan JavaScript murni. Anda sekarang memiliki pemahaman yang kuat tentang bagaimana komponen populer ini bekerja di balik layar. Anda telah belajar cara:

  • Menyusun HTML semantik untuk carousel.
  • Menggunakan CSS Flexbox dan transform untuk membuat efek geser.
  • Menggunakan overflow: hidden untuk menampilkan satu slide pada satu waktu.
  • Menulis logika JavaScript untuk mengontrol navigasi, memperbarui state, dan menangani interaksi pengguna.
  • Membuat komponen yang responsif dan menambahkan fitur opsional seperti auto-play.

Proyek ini adalah fondasi yang bagus. Jangan ragu untuk bereksperimen dengan ide-ide dari bagian “Penyempurnaan Lanjutan” untuk terus mengasah keterampilan Anda!


Kode Lengkap

Untuk referensi, berikut adalah kode lengkap untuk setiap file.

index.html

<!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 -->
    <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">
        <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>
            <li class="carousel-slide">
                <img src="images/slide4.jpg" alt="Gambar Slide 4">
            </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 titik akan dibuat oleh JavaScript -->
        </div>
    </div>

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

style.css

/* 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;
    width: 80%;
    max-width: 900px;
    height: 500px;
    overflow: hidden;
    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;
    transition: transform 0.5s ease-in-out;
}

.carousel-slide {
    min-width: 100%;
    height: 100%;
}

.carousel-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.carousel-button {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background-color: rgba(0,0,0,0.4);
    color: white;
    border: none;
    padding: 15px;
    cursor: pointer;
    z-index: 10;
    border-radius: 50%;
    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;
}

.carousel-button.is-hidden {
    display: 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);
    border: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    margin: 0 6px;
    cursor: pointer;
    padding: 0;
    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;
    transform: scale(1.2);
}

script.js

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

    // Buat indikator titik 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);

    let currentIndex = 0;

    const moveToSlide = (targetIndex) => {
        if (!track || !slides[targetIndex]) return;

        const currentSlideWidth = slides[targetIndex].getBoundingClientRect().width;
        track.style.transform = `translateX(-${currentSlideWidth * targetIndex}px)`;
        
        slides[currentIndex].classList.remove('current-slide');
        slides[targetIndex].classList.add('current-slide');
        
        dots[currentIndex].classList.remove('current-slide-indicator');
        dots[targetIndex].classList.add('current-slide-indicator');

        currentIndex = targetIndex;
        updateNavButtons();
    };

    const updateNavButtons = () => {
        if (currentIndex === 0) {
            prevButton.classList.add('is-hidden');
            nextButton.classList.remove('is-hidden');
        } else if (currentIndex === slides.length - 1) {
            nextButton.classList.add('is-hidden');
            prevButton.classList.remove('is-hidden');
        } else {
            prevButton.classList.remove('is-hidden');
            nextButton.classList.remove('is-hidden');
        }
    };

    updateNavButtons();

    nextButton.addEventListener('click', e => {
        if (currentIndex < slides.length - 1) {
            moveToSlide(currentIndex + 1);
        }
    });

    prevButton.addEventListener('click', e => {
        if (currentIndex > 0) {
            moveToSlide(currentIndex - 1);
        }
    });

    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);
        }
    });

    let resizeTimeout;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimeout);
        resizeTimeout = setTimeout(() => {
            moveToSlide(currentIndex);
        }, 250);
    });
    
    // --- Fitur Auto-Play (Opsional) ---
    let autoPlayInterval = null;
    const autoPlayDelay = 4000; // 4 detik

    function startAutoPlay() {
        if (autoPlayInterval) clearInterval(autoPlayInterval);
        
        autoPlayInterval = setInterval(() => {
            const nextIndex = (currentIndex + 1) % slides.length;
            moveToSlide(nextIndex);
        }, autoPlayDelay);
    }

    function stopAutoPlay() {
        clearInterval(autoPlayInterval);
    }

    // Perilaku auto-play:
    // 1. Jeda saat mouse di atas carousel
    carouselContainer.addEventListener('mouseenter', stopAutoPlay);
    // 2. Lanjutkan saat mouse meninggalkan carousel
    carouselContainer.addEventListener('mouseleave', startAutoPlay);
    // 3. Hentikan permanen saat pengguna klik tombol navigasi manual (opsional)
    // Untuk ini, tambahkan stopAutoPlay() di dalam event listener tombol.
    // Contoh: nextButton.addEventListener('click', e => { stopAutoPlay(); ... });

    // Mulai auto-play saat halaman dimuat
    startAutoPlay();
});

atau anda dapat temukan di repositori GitHub berikut: Source code lengkap

Diskusi

Tutorial Lainnya