Membuat Modal/Popup Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Membuat Modal/Popup Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Membuat Modal/Popup Kustom Responsif dengan HTML, CSS, & Vanilla JavaScript

Novian Hidayat
2025-05-12

Pelajari cara membuat komponen modal (popup) yang interaktif dan responsif dari awal. Panduan ini mencakup struktur HTML, styling CSS untuk overlay dan konten modal, serta logika JavaScript murni untuk membuka, menutup, dan meningkatkan aksesibilitas.

Membangun Modal/Popup Kustom yang Interaktif dan Aksesibel dengan HTML, CSS, dan Vanilla JS

Selamat datang di tutorial pembuatan modal kustom! Modal (atau popup) adalah jendela dialog yang muncul di atas konten halaman utama, biasanya untuk meminta input pengguna, menampilkan informasi penting, atau konfirmasi suatu tindakan. Membuat modal sendiri dari nol adalah cara terbaik untuk memahami cara kerjanya dan memastikan sesuai dengan kebutuhan desain Anda.

Mengapa Membuat Modal Kustom?

  • Kontrol Penuh: Anda menentukan setiap aspek tampilan dan perilakunya.
  • Ringan: Tidak ada dependensi atau kode berlebih dari library pihak ketiga.
  • Pembelajaran Aksesibilitas: Kesempatan bagus untuk mempraktikkan standar aksesibilitas web (a11y) untuk komponen interaktif.

Apa yang Akan Kita Buat?

Kita akan membuat modal serbaguna dengan fitur berikut:

  1. Struktur HTML yang jelas untuk modal dan pemicunya.
  2. Tombol Pemicu (Trigger) untuk membuka modal.
  3. Overlay Gelap di belakang modal untuk memfokuskan perhatian.
  4. Konten Modal yang bisa disesuaikan (judul, isi, tombol aksi).
  5. Tombol Tutup (Close Button) di dalam modal.
  6. Fungsionalitas Menutup Modal dengan:
    • Mengklik tombol tutup.
    • Mengklik area overlay.
    • Menekan tombol Escape pada keyboard.
  7. Transisi CSS yang halus untuk efek muncul dan hilang.
  8. Logika JavaScript murni untuk mengelola state dan interaksi.
  9. Pertimbangan Aksesibilitas Dasar: Fokus keyboard dan atribut ARIA.

Prasyarat

  • Text Editor: VS Code, Sublime Text, dll.
  • Web Browser: Chrome, Firefox, dll.
  • Pemahaman Dasar HTML dan CSS.
  • Pengetahuan Dasar JavaScript: Variabel, fungsi, event listener, manipulasi DOM (kelas, atribut).
  • (Opsional) Ikon: Font Awesome untuk ikon tombol tutup.

Mari kita mulai!


Langkah 1: Persiapan Proyek dan Struktur Folder

  1. Buat folder utama proyek, misalnya proyek-modal-kustom.
  2. Di dalamnya, buat:
    • index.html
    • style.css
    • script.js

Struktur dasar:

proyek-modal-kustom/
├── index.html
├── style.css
└── script.js

Langkah 2: Struktur Dasar HTML (index.html)

Buka index.html. Kita akan membuat tombol pemicu dan struktur untuk modal itu sendiri.

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

    <header>
        <h1>Contoh Halaman dengan Modal</h1>
    </header>

    <main>
        <p>Ini adalah konten utama halaman. Klik tombol di bawah untuk membuka modal.</p>
        <button class="open-modal-btn" data-modal-target="#myModal1">Buka Modal Informasi</button>
        <button class="open-modal-btn" data-modal-target="#myModal2">Buka Modal Konfirmasi</button>

        <!-- Konten lain di halaman -->
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </main>

    <!-- Struktur Modal 1: Informasi -->
    <div class="modal" id="myModal1" role="dialog" aria-modal="true" aria-labelledby="modal1Title" hidden>
        <div class="modal-overlay" data-modal-close></div>
        <div class="modal-content">
            <button class="modal-close-btn" data-modal-close aria-label="Tutup modal">
                <i class="fas fa-times"></i> <!-- Atau gunakan karakter '×' -->
            </button>
            <div class="modal-header">
                <h2 id="modal1Title">Informasi Penting</h2>
            </div>
            <div class="modal-body">
                <p>Ini adalah isi dari modal informasi. Anda bisa meletakkan teks, gambar, atau formulir di sini.</p>
                <p>Pastikan informasi yang ditampilkan jelas dan mudah dipahami.</p>
            </div>
            <div class="modal-footer">
                <button class="btn btn-primary" data-modal-close>Mengerti</button>
            </div>
        </div>
    </div>

    <!-- Struktur Modal 2: Konfirmasi (contoh lain) -->
    <div class="modal" id="myModal2" role="dialog" aria-modal="true" aria-labelledby="modal2Title" hidden>
        <div class="modal-overlay" data-modal-close></div>
        <div class="modal-content modal-sm"> <!-- Tambah kelas untuk ukuran berbeda -->
            <button class="modal-close-btn" data-modal-close aria-label="Tutup modal">&times;</button>
            <div class="modal-header">
                <h2 id="modal2Title">Konfirmasi Tindakan</h2>
            </div>
            <div class="modal-body">
                <p>Apakah Anda yakin ingin melanjutkan tindakan ini? Tindakan ini tidak dapat dibatalkan.</p>
            </div>
            <div class="modal-footer">
                <button class="btn btn-secondary" data-modal-close>Batal</button>
                <button class="btn btn-danger">Ya, Lanjutkan</button>
            </div>
        </div>
    </div>


    <footer>
        <p>&copy; 2025 Halaman Modal Kustom</p>
    </footer>

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

Penjelasan HTML:

  • Tombol Pemicu (.open-modal-btn):
    • data-modal-target="#myModal1": Atribut data kustom untuk menghubungkan tombol ini dengan modal spesifik yang memiliki ID myModal1. Ini memungkinkan kita memiliki banyak modal dan banyak pemicu.
  • Struktur Modal (.modal):
    • id="myModal1": ID unik untuk setiap modal.
    • role="dialog" dan aria-modal="true": Atribut ARIA penting untuk aksesibilitas, memberitahu teknologi pendukung bahwa ini adalah dialog modal.
    • aria-labelledby="modal1Title": Menghubungkan modal dengan judulnya untuk aksesibilitas.
    • hidden: Atribut HTML5 untuk menyembunyikan modal secara default (akan dikontrol oleh JS dan CSS).
  • Overlay (.modal-overlay): Elemen yang menutupi sisa halaman. data-modal-close adalah atribut data yang akan kita gunakan di JS untuk menandai elemen yang bisa menutup modal saat diklik.
  • Konten Modal (.modal-content): Wrapper untuk isi sebenarnya dari modal.
    • .modal-close-btn: Tombol untuk menutup modal. Juga memiliki data-modal-close. aria-label penting jika tombol hanya berisi ikon.
    • .modal-header, .modal-body, .modal-footer: Struktur umum untuk konten modal.
  • Kita membuat dua contoh modal (#myModal1 dan #myModal2) untuk menunjukkan fleksibilitas.

Langkah 3: Styling Dasar CSS (style.css)

Buka style.css. Kita akan mengatur tampilan awal modal (tersembunyi), overlay, dan konten modal.

/* style.css */
body {
    margin: 0;
    font-family: Arial, Helvetica, sans-serif;
    line-height: 1.6;
    color: #333;
}

header, main, footer {
    padding: 20px;
    max-width: 800px;
    margin: 0 auto;
}

.open-modal-btn, .btn {
    padding: 10px 20px;
    font-size: 1rem;
    cursor: pointer;
    border: none;
    border-radius: 5px;
    margin-right: 10px;
}

.open-modal-btn {
    background-color: #007bff;
    color: white;
}
.open-modal-btn:hover {
    background-color: #0056b3;
}

.btn-primary { background-color: #007bff; color: white; }
.btn-primary:hover { background-color: #0056b3; }
.btn-secondary { background-color: #6c757d; color: white; }
.btn-secondary:hover { background-color: #5a6268; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-danger:hover { background-color: #c82333; }


/* Styling Modal */
.modal {
    position: fixed; /* Tetap di layar saat scroll */
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex; /* Untuk menengahkan modal-content */
    align-items: center;
    justify-content: center;
    /* Awalnya tersembunyi, akan diubah oleh JS dan CSS transisi */
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s; /* Transisi untuk fade */
    z-index: 1000; /* Di atas segalanya */
}

.modal.is-open {
    visibility: visible;
    opacity: 1;
    transition-delay: 0s; /* Hapus delay saat membuka */
}

.modal-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.6); /* Warna overlay gelap transparan */
    cursor: pointer; /* Menunjukkan bisa diklik untuk menutup */
}

.modal-content {
    position: relative; /* Agar tombol close bisa diposisikan absolut terhadap ini */
    background-color: white;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.3);
    width: 90%;
    max-width: 600px; /* Lebar maksimum modal */
    z-index: 1001; /* Di atas overlay */
    transform: translateY(-50px) scale(0.95); /* Efek awal sebelum muncul */
    opacity: 0; /* Awalnya transparan */
    transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}

.modal.is-open .modal-content {
    transform: translateY(0) scale(1); /* Efek muncul */
    opacity: 1;
}

/* Ukuran modal berbeda (opsional) */
.modal-content.modal-sm {
    max-width: 400px;
}
.modal-content.modal-lg {
    max-width: 800px;
}


.modal-close-btn {
    position: absolute;
    top: 15px;
    right: 15px;
    background: none;
    border: none;
    font-size: 1.5rem; /* Ukuran ikon close */
    color: #888;
    cursor: pointer;
    padding: 0;
    line-height: 1; /* Agar ikon tidak punya spasi ekstra */
}
.modal-close-btn:hover {
    color: #333;
}

.modal-header {
    border-bottom: 1px solid #eee;
    padding-bottom: 15px;
    margin-bottom: 20px;
}
.modal-header h2 {
    margin: 0;
    font-size: 1.5rem;
}

.modal-body {
    margin-bottom: 20px;
}
.modal-body p:last-child {
    margin-bottom: 0;
}

.modal-footer {
    border-top: 1px solid #eee;
    padding-top: 15px;
    text-align: right; /* Tombol footer di kanan */
}
.modal-footer .btn {
    margin-left: 10px;
}
.modal-footer .btn:first-child {
    margin-left: 0;
}

Penjelasan CSS Awal:

  • .modal:
    • position: fixed agar menutupi seluruh viewport dan tetap saat di-scroll.
    • display: flex, align-items: center, justify-content: center untuk menengahkan .modal-content.
    • Awalnya visibility: hidden dan opacity: 0. Transisi diatur agar visibility berubah setelah opacity (untuk mencegah interaksi dengan elemen tak terlihat).
  • .modal.is-open: Kelas yang akan ditambahkan oleh JavaScript untuk menampilkan modal. Properti transisi akan bekerja di sini.
  • .modal-overlay: Menutupi seluruh layar di belakang konten modal.
  • .modal-content:
    • position: relative untuk positioning tombol close.
    • transform dan opacity diatur untuk efek animasi muncul (misalnya, sedikit dari atas dan membesar).
  • .modal.is-open .modal-content: Mengembalikan transform dan opacity ke nilai normal saat modal terbuka.

Langkah 4: JavaScript - Logika Membuka dan Menutup Modal

Buka script.js. Kita akan menulis logika untuk mengontrol visibilitas modal.

// script.js
document.addEventListener('DOMContentLoaded', () => {
    const openModalButtons = document.querySelectorAll('[data-modal-target]');
    const closeModalButtons = document.querySelectorAll('[data-modal-close]'); // Termasuk overlay dan tombol close

    let previouslyFocusedElement = null; // Untuk menyimpan elemen yang fokus sebelum modal dibuka

    // Fungsi untuk membuka modal
    function openModal(modal) {
        if (modal == null) return;

        // Simpan elemen yang sedang fokus
        previouslyFocusedElement = document.activeElement;

        modal.removeAttribute('hidden'); // Hapus atribut hidden dulu
        // Beri sedikit waktu agar display berubah sebelum memulai transisi CSS
        requestAnimationFrame(() => {
            modal.classList.add('is-open');
            // Fokuskan elemen pertama yang bisa difokus di dalam modal
            const focusableElements = modal.querySelectorAll(
                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
            );
            if (focusableElements.length > 0) {
                focusableElements[0].focus();
            } else {
                modal.focus(); // Jika tidak ada, fokuskan modal itu sendiri (perlu tabindex="-1" di modal)
            }
        });
        document.body.style.overflow = 'hidden'; // Mencegah scroll di body saat modal terbuka
    }

    // Fungsi untuk menutup modal
    function closeModal(modal) {
        if (modal == null) return;
        modal.classList.remove('is-open');

        // Kembalikan fokus ke elemen sebelumnya
        if (previouslyFocusedElement) {
            previouslyFocusedElement.focus();
            previouslyFocusedElement = null;
        }

        // Tunggu transisi selesai sebelum menambahkan atribut 'hidden' lagi
        // Durasi transisi opacity modal adalah 0.3s (300ms)
        modal.addEventListener('transitionend', function handler() {
            modal.setAttribute('hidden', true);
            modal.removeEventListener('transitionend', handler); // Hapus listener setelah selesai
        }, { once: true }); // Opsi {once: true} juga bisa, tapi beberapa browser lama mungkin tidak support

        document.body.style.overflow = ''; // Kembalikan scroll body
    }

    // Event listener untuk tombol-tombol pembuka modal
    openModalButtons.forEach(button => {
        button.addEventListener('click', () => {
            const modalId = button.dataset.modalTarget;
            const modal = document.querySelector(modalId);
            if (modal) {
                openModal(modal);
            }
        });
    });

    // Event listener untuk elemen penutup modal (overlay, tombol close)
    closeModalButtons.forEach(element => {
        element.addEventListener('click', () => {
            // Cari elemen modal terdekat yang menampung tombol/overlay ini
            const modal = element.closest('.modal');
            if (modal) {
                closeModal(modal);
            }
        });
    });

    // Event listener untuk tombol Escape (Esc)
    document.addEventListener('keydown', (event) => {
        if (event.key === 'Escape' || event.key === 'Esc') {
            // Cari modal yang sedang terbuka
            const openModals = document.querySelectorAll('.modal.is-open');
            openModals.forEach(modal => {
                closeModal(modal);
            });
        }
    });

    // (Opsional) Menutup modal jika diklik di luar modal-content (hanya jika klik di overlay)
    // Ini sudah di-handle oleh data-modal-close pada .modal-overlay
    // Jika ingin lebih spesifik:
    // document.querySelectorAll('.modal').forEach(modal => {
    //     modal.addEventListener('click', (event) => {
    //         if (event.target === modal) { // Hanya jika klik langsung pada .modal (overlay)
    //             closeModal(modal);
    //         }
    //     });
    // });
});

Penjelasan JavaScript:

  1. Seleksi Elemen: Mengambil semua tombol pemicu (data-modal-target) dan semua elemen yang bisa menutup modal (data-modal-close).
  2. previouslyFocusedElement: Variabel untuk menyimpan elemen yang memiliki fokus sebelum modal dibuka. Ini penting untuk mengembalikan fokus saat modal ditutup (baik untuk aksesibilitas).
  3. openModal(modal):
    • Menghapus atribut hidden.
    • Menggunakan requestAnimationFrame untuk memastikan browser telah memproses perubahan display sebelum menambahkan kelas is-open (untuk transisi CSS yang benar).
    • Menambahkan kelas is-open untuk memicu transisi CSS.
    • Fokus Manajemen: Mencari elemen pertama yang bisa difokuskan di dalam modal dan memberinya fokus. Jika tidak ada, modal itu sendiri bisa difokuskan (memerlukan tabindex="-1" pada elemen .modal di HTML).
    • document.body.style.overflow = 'hidden': Mencegah konten halaman di belakang modal di-scroll.
  4. closeModal(modal):
    • Menghapus kelas is-open.
    • Mengembalikan Fokus: Memberikan fokus kembali ke previouslyFocusedElement.
    • Menunggu transisi CSS selesai (berdasarkan durasi transisi opacity modal) sebelum menambahkan kembali atribut hidden. Ini penting agar animasi fade-out terlihat.
    • document.body.style.overflow = '': Mengembalikan kemampuan scroll pada body.
  5. Event Listeners:
    • Untuk setiap openModalButton: Mengambil target modal dari data-modal-target dan memanggil openModal.
    • Untuk setiap closeModalButton (termasuk overlay): Menggunakan element.closest('.modal') untuk menemukan modal induk dan memanggil closeModal.
    • Untuk tombol Escape: Mencari semua modal yang sedang terbuka dan menutupnya.

Langkah 5: Meningkatkan Aksesibilitas (Focus Trapping)

Salah satu aspek penting aksesibilitas modal adalah “focus trapping”. Artinya, saat modal terbuka, pengguna tidak bisa menekan tombol Tab untuk berinteraksi dengan elemen di belakang modal. Fokus harus tetap terperangkap di dalam modal.

Ini sedikit lebih kompleks dan bisa ditambahkan sebagai peningkatan. Berikut adalah implementasi dasar focus trapping:

Tambahkan fungsi ini dan panggil di dalam openModal dan closeModal:

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

    // --- Focus Trapping ---
    let currentOpenModal = null; // Menyimpan referensi modal yang sedang terbuka

    function trapFocus(modal) {
        const focusableElementsString = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
        const focusableElements = Array.from(modal.querySelectorAll(focusableElementsString));
        if (focusableElements.length === 0) return;

        const firstFocusableElement = focusableElements[0];
        const lastFocusableElement = focusableElements[focusableElements.length - 1];

        // Fokuskan elemen pertama saat modal dibuka (sudah ada di openModal)
        // firstFocusableElement.focus();

        const handleTabKeyPress = (event) => {
            if (event.key === 'Tab') {
                if (event.shiftKey) { // Shift + Tab
                    if (document.activeElement === firstFocusableElement) {
                        event.preventDefault();
                        lastFocusableElement.focus();
                    }
                } else { // Tab
                    if (document.activeElement === lastFocusableElement) {
                        event.preventDefault();
                        firstFocusableElement.focus();
                    }
                }
            }
        };

        modal.addEventListener('keydown', handleTabKeyPress);
        // Simpan referensi fungsi agar bisa dihapus saat modal ditutup
        modal._handleTabKeyPress = handleTabKeyPress; 
    }

    function releaseFocus(modal) {
        if (modal && modal._handleTabKeyPress) {
            modal.removeEventListener('keydown', modal._handleTabKeyPress);
            delete modal._handleTabKeyPress;
        }
    }

    // Modifikasi fungsi openModal dan closeModal

    // Di dalam fungsi openModal(modal):
    // Setelah: modal.classList.add('is-open');
    // Tambahkan:
    //     currentOpenModal = modal;
    //     trapFocus(modal);
    //     // Fokuskan elemen pertama (sudah ada)

    // Di dalam fungsi closeModal(modal):
    // Sebelum: modal.classList.remove('is-open');
    // Tambahkan:
    //     releaseFocus(modal);
    //     currentOpenModal = null;

Pembaruan openModal dan closeModal dengan Focus Trapping:

// script.js
document.addEventListener('DOMContentLoaded', () => {
    const openModalButtons = document.querySelectorAll('[data-modal-target]');
    const closeModalButtons = document.querySelectorAll('[data-modal-close]');

    let previouslyFocusedElement = null;
    let currentOpenModal = null; // Untuk focus trapping

    // --- Focus Trapping Functions ---
    function trapFocus(modal) {
        const focusableElementsString = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
        const focusableElements = Array.from(modal.querySelectorAll(focusableElementsString));
        if (focusableElements.length === 0) return;

        const firstFocusableElement = focusableElements[0];
        const lastFocusableElement = focusableElements[focusableElements.length - 1];

        const handleTabKeyPress = (event) => {
            if (!modal.classList.contains('is-open')) return; // Hanya jika modal terbuka

            if (event.key === 'Tab') {
                // Jika tidak ada elemen yang bisa difokus atau hanya satu, jangan lakukan apa-apa
                if (focusableElements.length <= 1) {
                    event.preventDefault();
                    return;
                }

                if (event.shiftKey) { // Shift + Tab
                    if (document.activeElement === firstFocusableElement) {
                        event.preventDefault();
                        lastFocusableElement.focus();
                    }
                } else { // Tab
                    if (document.activeElement === lastFocusableElement) {
                        event.preventDefault();
                        firstFocusableElement.focus();
                    }
                }
            }
        };
        // Gunakan document untuk event keydown agar bisa menangkap tab dari luar modal juga
        // dan mencegahnya jika modal terbuka
        document.addEventListener('keydown', handleTabKeyPress);
        modal._handleTabKeyPress = handleTabKeyPress; // Simpan referensi untuk remove
    }

    function releaseFocus(modal) {
        if (modal && modal._handleTabKeyPress) {
            document.removeEventListener('keydown', modal._handleTabKeyPress);
            delete modal._handleTabKeyPress;
        }
    }

    // Fungsi untuk membuka modal
    function openModal(modal) {
        if (modal == null || modal.classList.contains('is-open')) return;

        previouslyFocusedElement = document.activeElement;
        currentOpenModal = modal; // Set modal yang aktif

        modal.removeAttribute('hidden');
        requestAnimationFrame(() => {
            modal.classList.add('is-open');
            trapFocus(modal); // Mulai focus trapping

            const focusableElements = modal.querySelectorAll(
                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
            );
            if (focusableElements.length > 0) {
                focusableElements[0].focus();
            } else {
                 // Beri modal tabindex agar bisa difokuskan jika tidak ada elemen lain
                modal.setAttribute('tabindex', '-1');
                modal.focus();
            }
        });
        document.body.style.overflow = 'hidden';
    }

    // Fungsi untuk menutup modal
    function closeModal(modal) {
        if (modal == null || !modal.classList.contains('is-open')) return;

        releaseFocus(modal); // Hentikan focus trapping
        currentOpenModal = null; // Reset modal yang aktif

        modal.classList.remove('is-open');

        if (previouslyFocusedElement) {
            previouslyFocusedElement.focus();
            previouslyFocusedElement = null;
        }

        modal.addEventListener('transitionend', function handler() {
            modal.setAttribute('hidden', true);
            if (!modal.classList.contains('is-open') && modal.hasAttribute('tabindex')) {
                modal.removeAttribute('tabindex'); // Hapus tabindex jika ditambahkan
            }
            modal.removeEventListener('transitionend', handler);
        }, { once: true });

        document.body.style.overflow = '';
    }

    // Event listener untuk tombol-tombol pembuka modal
    openModalButtons.forEach(button => {
        button.addEventListener('click', () => {
            const modalId = button.dataset.modalTarget;
            const modal = document.querySelector(modalId);
            if (modal) {
                openModal(modal);
            }
        });
    });

    // Event listener untuk elemen penutup modal (overlay, tombol close)
    closeModalButtons.forEach(element => {
        element.addEventListener('click', () => {
            // Cari elemen modal terdekat yang menampung tombol/overlay ini
            const modal = element.closest('.modal');
            if (modal) {
                closeModal(modal);
            }
        });
    });

    // Event listener untuk tombol Escape (Esc)
    document.addEventListener('keydown', (event) => {
        if ((event.key === 'Escape' || event.key === 'Esc') && currentOpenModal) {
            closeModal(currentOpenModal); // Tutup modal yang sedang aktif
        }
    });
});

Perubahan Penting untuk Focus Trapping:

  • trapFocus sekarang menambahkan event listener keydown ke document agar bisa menangkap event Tab bahkan jika fokus mencoba keluar dari modal.
  • Logika di dalam handleTabKeyPress memeriksa apakah modal sedang terbuka sebelum melakukan trapping.
  • Pastikan releaseFocus benar-benar menghapus listener dari document.
  • Jika tidak ada elemen yang bisa difokus di modal, kita tambahkan tabindex="-1" ke modal itu sendiri agar bisa difokuskan, dan menghapusnya saat modal ditutup.

Langkah 6: Menguji Modal Anda

Simpan semua file dan buka index.html di browser.

  • Klik tombol “Buka Modal”. Modal seharusnya muncul dengan efek transisi.
  • Overlay gelap seharusnya menutupi halaman.
  • Coba tutup modal dengan:
    • Tombol “X” atau tombol close di footer.
    • Mengklik area overlay.
    • Menekan tombol Escape.
  • Perhatikan animasi saat membuka dan menutup.
  • Uji Aksesibilitas:
    • Saat modal terbuka, coba tekan Tab. Fokus seharusnya berpindah antar elemen yang bisa difokus di dalam modal saja (tombol close, input, tombol footer), dan tidak ke elemen di belakang modal.
    • Tekan Shift + Tab untuk bergerak mundur.
    • Saat modal ditutup, fokus seharusnya kembali ke tombol yang tadi Anda klik untuk membuka modal.

Kesimpulan dan Langkah Selanjutnya

Selamat! Anda telah berhasil membuat komponen modal kustom yang fungsional, responsif, dan mempertimbangkan aspek aksesibilitas dasar. Ini adalah keterampilan penting untuk developer front-end.

Apa yang telah dipelajari:

  • Struktur HTML semantik untuk modal dan atribut ARIA.
  • Styling CSS untuk overlay, konten modal, dan transisi animasi.
  • Logika JavaScript untuk mengelola state buka/tutup.
  • Menangani berbagai cara menutup modal (tombol, overlay, Esc).
  • Manajemen fokus untuk pengalaman pengguna dan aksesibilitas yang lebih baik.
  • Implementasi dasar focus trapping.

Langkah Selanjutnya yang Bisa Anda Jelajahi:

  1. Modal Bertingkat (Nested Modals): Bagaimana jika satu modal membuka modal lain? Perlu penanganan state dan fokus yang lebih cermat.
  2. Konten Dinamis: Memuat konten modal menggunakan AJAX/Fetch dari server.
  3. Animasi Lebih Kompleks: Eksplorasi efek animasi yang berbeda dengan CSS.
  4. Integrasi dengan Framework: Menerapkan konsep serupa dalam framework seperti React, Vue, atau Angular, yang biasanya memiliki solusi bawaan atau pola untuk komponen modal.
  5. Validasi Form di Dalam Modal: Jika modal berisi formulir.
  6. Pengujian Aksesibilitas Lebih Lanjut: Gunakan screen reader untuk menguji pengalaman pengguna dengan modal Anda.

Teruslah berlatih dan membangun komponen UI kustom. Ini akan sangat meningkatkan pemahaman Anda tentang cara kerja web. Semoga berhasil!

Tutorial Terkait