kodingup.camp

Beranda/Kualitas Web: Aksesibilitas, Performa, Keamanan, Testing & Tooling

Aksesibilitas dalam Praktik

Modul 2 dari 88 menit bacaAccessible HTML, CSS and JS

Isi modul

    Kenapa ini penting

    Modul sebelumnya memberimu peta. Modul ini memberimu kotak perkakas. Ibaratnya seperti belajar menyetir: tahu rambu lalu lintas itu penting, tapi yang membuatmu lulus ujian SIM adalah kebiasaan kecil yang dilakukan berulang, seperti melihat spion, menyalakan sein, dan menjaga jarak. Dalam aksesibilitas, kebiasaan kecilnya adalah: setiap input punya label, setiap gambar punya alt yang tepat, warna punya kontras cukup, fokus selalu terlihat, semua fitur jalan dengan keyboard, dan animasi menghormati pengaturan pengguna.

    Hampir semua kegagalan di laporan WebAIM bisa diperbaiki dengan teknik di modul ini, dan sebagian besar hanya butuh satu atribut atau beberapa baris CSS. Di akhir modul kita memakai Lighthouse dan axe DevTools untuk mengaudit hasilnya.

    Konsep inti

    1. Label form: setiap input harus punya nama

    Placeholder bukan label. Placeholder hilang saat pengguna mulai mengetik (pengguna lupa kolom itu untuk apa), warnanya biasanya terlalu pucat, dan tidak selalu dibacakan dengan andal. Pakai <label> yang dihubungkan lewat for dan id:

    <label for="email">Email</label>
    <input id="email" type="email" autocomplete="email" required />
    

    Bonusnya, mengklik teks label akan memfokuskan input, jadi area klik jadi lebih besar. Hal lain yang perlu diperhatikan:

    2. Teks alternatif (alt) yang tepat

    Atribut alt menjawab pertanyaan: "Kalau gambar ini tidak terlihat, informasi apa yang hilang?"

    Jenis gambar Contoh alt yang tepat
    Informatif Foto produk alt="Sepatu lari biru navy dengan sol putih"
    Fungsional (di dalam link/tombol) Ikon kaca pembesar di tombol cari alt="Cari", jelaskan fungsinya, bukan bentuknya
    Dekoratif Garis ornamen, foto latar suasana alt="" (kosong, tapi atributnya tetap ada)
    Kompleks Grafik penjualan alt singkat plus penjelasan lengkap di teks/tabel dekatnya

    Jangan menulis "gambar dari..." atau "foto..." karena screen reader sudah menyebut "image". Dan jangan menghapus atribut alt sama sekali, karena screen reader lalu membacakan nama file. Untuk ikon SVG di dalam tombol yang sudah punya teks, sembunyikan ikonnya dengan aria-hidden="true".

    3. Kontras warna

    WCAG 2.2 kriteria 1.4.3 (AA) mensyaratkan rasio kontras minimal 4.5:1 untuk teks biasa dan 3:1 untuk teks besar (minimal 18pt, atau 14pt tebal, kira-kira 24px atau 18,66px bold). Kriteria 1.4.11 (AA) mensyaratkan 3:1 untuk komponen UI dan grafik penting, misalnya border input dan ikon. Rasio dihitung dari luminansi relatif dua warna, dari 1:1 (sama persis) sampai 21:1 (hitam di atas putih). Beberapa contoh di latar putih yang sudah kita hitung:

    Warna teks Rasio di atas #fff Status teks biasa (4.5:1)
    #aaaaaa (abu muda) 2.32:1 ❌ Gagal
    #9ca3af 2.54:1 ❌ Gagal
    #777777 4.48:1 ❌ Gagal tipis
    #767676 4.54:1 ✅ Lulus (abu paling terang yang lulus)
    #6b7280 4.83:1 ✅ Lulus
    #1f2937 (abu gelap) 14.68:1 ✅ Lulus

    Kriteria 1.4.1 (A) juga melarang memakai warna sebagai satu-satunya penanda. Field yang error jangan hanya diberi border merah. Tambahkan teks pesan dan, kalau perlu, ikon. Orang dengan buta warna merah-hijau tidak bisa membedakan border merah dan hijau, dan saat layar dicetak hitam-putih semua orang kehilangan informasi itu.

    4. Fokus dan keyboard

    Banyak orang menavigasi hanya dengan keyboard: Tab maju, Shift+Tab mundur, Enter mengaktifkan link/tombol, Spasi menekan tombol atau mencentang checkbox, panah di dalam grup radio atau menu, Esc menutup dialog. Aturan yang harus dijaga:

    5. ARIA secukupnya

    ARIA (Accessible Rich Internet Applications) adalah sekumpulan atribut (role, aria-*) untuk menambah atau mengubah informasi di accessibility tree. ARIA tidak menambah perilaku: role="button" di <div> membuat screen reader menyebutnya tombol, tapi div itu tetap tidak bisa difokus atau ditekan dengan Enter sampai kamu menambahkannya sendiri. Karena itu ingat aturan pertama: pakai HTML bawaan dulu. ARIA yang paling sering berguna:

    Atribut Kegunaan Contoh
    aria-label Nama untuk elemen tanpa teks terlihat <button aria-label="Tutup">×</button>
    aria-labelledby Ambil nama dari elemen lain Dialog yang dinamai dari judulnya
    aria-describedby Deskripsi tambahan Hubungkan input ke pesan error/petunjuk
    aria-expanded Status buka/tutup Tombol menu hamburger
    aria-invalid Input tidak valid Setelah validasi gagal
    aria-live / role="status" Umumkan perubahan konten tanpa memindah fokus "3 produk ditambahkan ke keranjang"
    aria-hidden="true" Sembunyikan dari accessibility tree Ikon dekoratif. Jangan pakai pada elemen yang bisa difokus
    aria-current="page" Tandai halaman aktif di navigasi Link menu halaman saat ini

    Satu peringatan: ARIA yang salah lebih buruk daripada tanpa ARIA, karena kamu memberi informasi keliru ke pengguna yang paling bergantung padanya.

    6. Menghormati prefers-reduced-motion

    Sebagian orang dengan gangguan vestibular merasa pusing atau mual melihat animasi parallax, zoom, atau geser besar. Sistem operasi punya pengaturan "kurangi gerakan" (Reduce motion di macOS/iOS, "Remove animations" di Android, "Animation effects" di Windows). CSS bisa membacanya lewat media query prefers-reduced-motion. Pola paling aman adalah opt-in: animasi hanya diaktifkan kalau pengguna tidak meminta pengurangan gerakan. Kalau butuh di JavaScript, pakai matchMedia('(prefers-reduced-motion: reduce)').

    7. Mengaudit: otomatis dan manual

    Kenapa manual tetap perlu? Kami menguji axe pada halaman yang sengaja dibuat buruk. axe menemukan gambar tanpa alt, tombol tanpa nama, kontras rendah, <html> tanpa lang, dan halaman tanpa <h1>. Tapi <div onclick> yang tidak bisa diakses keyboard lolos tanpa peringatan, begitu pula input yang hanya punya placeholder (axe menerima placeholder sebagai nama). Alat otomatis adalah lantai, bukan langit-langit.

    Contoh kode

    Form newsletter berikut dipakai lagi di modul testing. Perhatikan komentar di setiap atribut aksesibilitas.

    <!-- index.html -->
    <!doctype html>
    <html lang="id">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="description" content="Daftar newsletter mingguan" />
        <title>Daftar Newsletter</title>
      </head>
      <body>
        <!-- Elemen pertama yang difokus Tab: lompat melewati menu -->
        <a class="skip-link" href="#main">Lewati ke konten utama</a>
        <main id="main">
          <h1>Daftar Newsletter</h1>
          <div id="app"></div>
        </main>
        <script type="module" src="/src/main.js"></script>
      </body>
    </html>
    
    // src/form.js
    import { isEmailValid, escapeHTML } from './utils.js';
    
    export function setupForm(root) {
      root.innerHTML = `
        <form novalidate>
          <label for="email">Email</label>
          <input id="email" name="email" type="email" autocomplete="email"
                 aria-describedby="email-error" required>
          <p id="email-error" class="error" hidden></p>
          <button type="submit">Daftar</button>
        </form>
        <p class="status" role="status"></p>
      `;
      // novalidate: kita pakai pesan error sendiri yang bahasanya lebih jelas
      // aria-describedby: screen reader membacakan pesan error saat input difokus
      // role="status": perubahan teks di sini diumumkan tanpa memindah fokus
    
      const form = root.querySelector('form');
      const input = root.querySelector('#email');
      const error = root.querySelector('#email-error');
      const status = root.querySelector('.status');
    
      form.addEventListener('submit', (event) => {
        event.preventDefault();
        if (!isEmailValid(input.value)) {
          // Pesan berupa TEKS yang menjelaskan cara memperbaiki, bukan cuma warna merah
          error.textContent = 'Format email belum benar, contoh: budi@contoh.id';
          error.hidden = false;
          input.setAttribute('aria-invalid', 'true');
          input.focus(); // bawa pengguna langsung ke sumber masalah
          return;
        }
        error.hidden = true;
        input.removeAttribute('aria-invalid');
        // escapeHTML mencegah XSS (dibahas di modul Keamanan)
        status.innerHTML = `Terima kasih, <strong>${escapeHTML(input.value)}</strong> sudah terdaftar.`;
        form.reset();
      });
    }
    
    /* src/style.css */
    :root {
      --teks: #1f2937;   /* 14.68:1 di atas putih */
      --latar: #fff;
      --aksen: #1d4ed8;  /* teks putih di atasnya: 6.7:1 */
      --error: #b91c1c;  /* 6.47:1 di atas putih */
    }
    
    body {
      margin: 0 auto;
      max-width: 40rem;
      padding: 1rem;
      font-family: system-ui, sans-serif;
      color: var(--teks);
      background: var(--latar);
    }
    
    /* Skip link: tersembunyi di luar layar, muncul saat difokus keyboard */
    .skip-link {
      position: absolute;
      left: -999px;
    }
    
    .skip-link:focus {
      left: 1rem;
      top: 1rem;
    }
    
    input,
    button {
      font: inherit;
      min-height: 44px; /* jauh di atas minimum 24px WCAG 2.5.8 */
      padding: 0 0.75rem;
    }
    
    button {
      background: var(--aksen);
      color: #fff;
      border: 0;
      border-radius: 6px;
      user-select: none;
    }
    
    /* Cincin fokus jelas, hanya muncul saat navigasi keyboard */
    :focus-visible {
      outline: 3px solid var(--aksen);
      outline-offset: 2px;
    }
    
    .error {
      color: var(--error);
    }
    
    /* Animasi hanya untuk yang TIDAK minta reduced motion (pola opt-in) */
    @media (prefers-reduced-motion: no-preference) {
      button {
        transition: transform 150ms ease;
      }
    
      button:hover {
        transform: translateY(-1px);
      }
    }
    

    Di versi JavaScript, cek preferensi yang sama sebelum menjalankan animasi:

    const kurangiGerak = window.matchMedia('(prefers-reduced-motion: reduce)');
    
    function scrollKeAtas() {
      // 'instant' kalau pengguna minta tanpa animasi, 'smooth' kalau tidak
      window.scrollTo({ top: 0, behavior: kurangiGerak.matches ? 'instant' : 'smooth' });
    }
    
    kurangiGerak.addEventListener('change', () => {
      console.log('Preferensi gerak berubah:', kurangiGerak.matches);
    });
    // → Preferensi gerak berubah: true   (setelah "Reduce motion" dinyalakan di pengaturan OS)
    

    Untuk mengaudit, jalankan halaman dengan npm run build && npm run preview, lalu buka DevTools → tab Lighthouse → centang Accessibility → Analyze page load. Versi CLI-nya:

    # Butuh Google Chrome terpasang. Lighthouse 13.5 saat materi ini ditulis
    npx lighthouse http://localhost:4173 --only-categories=accessibility --view
    # → membuka laporan HTML. Halaman newsletter di atas mendapat skor Accessibility 100
    

    Kesalahan umum

    1. outline: none di reset CSS. Pengguna keyboard jadi "buta" tidak tahu sedang di mana. Ganti dengan gaya :focus-visible yang jelas, kontras minimal 3:1.
    2. Placeholder sebagai pengganti label. Selalu pakai <label for>. Kalau desain tidak mau label terlihat, sembunyikan secara visual dengan kelas .sr-only (bukan display: none, yang ikut menyembunyikan dari screen reader).
    3. alt yang mengulang konteks atau nama file. alt="IMG_2031.jpg" atau alt="gambar" tidak membantu. Untuk gambar dekoratif pakai alt="".
    4. Menempel role="button" ke div lalu berhenti. Tanpa tabindex="0" dan handler Enter/Spasi, elemen itu tetap tidak bisa dipakai keyboard. Solusi paling sederhana: pakai <button>.
    5. Merasa aman setelah skor Lighthouse 100. Skor itu hanya mencakup cek otomatis. Uji dengan Tab, zoom 200%, dan screen reader.

    Latihan

    1. Mudah, Perbaiki: <img src="logo.png"> (logo yang juga link ke beranda) dan <input placeholder="Nama lengkap">.
    2. Sedang, Buat tombol menu hamburger yang aksesibel: tombol dengan nama "Menu", aria-expanded yang berubah saat diklik, dan daftar menu yang tampil/sembunyi.
    3. Menantang, Buat fungsi rasioKontras(hex1, hex2) di JavaScript yang menghitung rasio kontras WCAG, lalu uji: #767676 di atas #ffffff harus menghasilkan sekitar 4.54.
    Lihat solusi

    1.

    <a href="/"><img src="logo.png" alt="TokoKita, ke beranda" width="120" height="40"></a>
    
    <label for="nama">Nama lengkap</label>
    <input id="nama" name="nama" autocomplete="name" placeholder="contoh: Budi Santoso">
    

    2.

    <button type="button" id="tombol-menu" aria-expanded="false" aria-controls="menu">Menu</button>
    <ul id="menu" hidden>
      <li><a href="/">Beranda</a></li>
      <li><a href="/produk">Produk</a></li>
    </ul>
    <script type="module">
      const tombol = document.querySelector('#tombol-menu');
      const menu = document.querySelector('#menu');
      tombol.addEventListener('click', () => {
        const terbuka = tombol.getAttribute('aria-expanded') === 'true';
        tombol.setAttribute('aria-expanded', String(!terbuka));
        menu.hidden = terbuka;
      });
      // Esc menutup menu dan mengembalikan fokus ke tombol
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && !menu.hidden) {
          menu.hidden = true;
          tombol.setAttribute('aria-expanded', 'false');
          tombol.focus();
        }
      });
    </script>
    

    3.

    function luminansi(hex) {
      let h = hex.replace('#', '');
      if (h.length === 3) h = [...h].map((c) => c + c).join('');
      const [r, g, b] = [0, 2, 4].map((i) => {
        const c = parseInt(h.slice(i, i + 2), 16) / 255;
        return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
      });
      return 0.2126 * r + 0.7152 * g + 0.0722 * b;
    }
    
    function rasioKontras(hex1, hex2) {
      const [terang, gelap] = [luminansi(hex1), luminansi(hex2)].sort((a, b) => b - a);
      return Math.round(((terang + 0.05) / (gelap + 0.05)) * 100) / 100;
    }
    
    console.log(rasioKontras('#767676', '#ffffff')); // → 4.54
    console.log(rasioKontras('#aaa', '#fff'));       // → 2.32
    

    Ringkasan

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    The Only Accessibility Video You Will Ever NeedWeb Dev Simplified · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.