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:
- Kelompokkan radio dan checkbox dengan
<fieldset>dan<legend>, supaya screen reader membacakan pertanyaannya ("Metode pengiriman: Reguler, radio button, 1 dari 2"). autocomplete(email,name,tel,street-address) membantu pengguna dengan gangguan motorik dan kognitif karena browser bisa mengisi otomatis.- Pesan error harus berupa teks (bukan hanya border merah), dihubungkan ke input dengan
aria-describedby, dan input ditandaiaria-invalid="true". Pindahkan fokus ke input yang salah supaya pengguna langsung tahu.
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:
- Semua yang bisa diklik harus bisa dicapai dan diaktifkan dengan keyboard (2.1.1, A).
- Tidak boleh ada jebakan keyboard: fokus yang masuk ke suatu komponen harus bisa keluar lagi (2.1.2, A).
- Fokus harus terlihat (2.4.7, AA). Jangan pernah menulis
outline: nonetanpa pengganti. Pakai:focus-visible, yang hanya menampilkan cincin fokus saat pengguna memakai keyboard, sehingga desainer tidak terganggu oleh cincin saat mengklik dengan mouse. - Fokus tidak tertutup header sticky atau banner cookie (2.4.11, AA). Kalau perlu, pakai
scroll-padding-topdihtml. - Urutan fokus mengikuti urutan baca (2.4.3, A). Atur dengan urutan DOM, bukan
tabindexpositif. Gunakantabindex="0"hanya untuk membuat elemen kustom bisa difokus, dantabindex="-1"untuk elemen yang ingin difokus lewat JavaScript (misalnya judul setelah navigasi) tanpa masuk urutan Tab. - Skip link ("Lewati ke konten utama") sebagai elemen pertama membantu pengguna keyboard melewati menu panjang (2.4.1, A).
- Target sentuh minimal 24×24 piksel CSS (2.5.8, AA). Banyak panduan desain merekomendasikan 44×44 agar lebih nyaman.
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
- Lighthouse (tab Lighthouse di Chrome DevTools, atau
npx lighthouse <url>) memberi skor kategori Accessibility beserta daftar masalahnya. - axe DevTools (ekstensi browser dari Deque) dan pustaka axe-core (dipakai Lighthouse di balik layar) memberi laporan lebih rinci per elemen. Di modul E2E kita memakai
@axe-core/playwrightagar audit jalan otomatis di setiap test. - Panel Accessibility dan CSS Overview di DevTools menunjukkan nama aksesibel dan masalah kontras.
- Uji manual tetap wajib: cabut mouse dan pakai halaman hanya dengan keyboard selama 5 menit, zoom ke 200%, lalu coba screen reader.
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
outline: nonedi reset CSS. Pengguna keyboard jadi "buta" tidak tahu sedang di mana. Ganti dengan gaya:focus-visibleyang jelas, kontras minimal 3:1.- Placeholder sebagai pengganti label. Selalu pakai
<label for>. Kalau desain tidak mau label terlihat, sembunyikan secara visual dengan kelas.sr-only(bukandisplay: none, yang ikut menyembunyikan dari screen reader). altyang mengulang konteks atau nama file.alt="IMG_2031.jpg"ataualt="gambar"tidak membantu. Untuk gambar dekoratif pakaialt="".- Menempel
role="button"kedivlalu berhenti. Tanpatabindex="0"dan handler Enter/Spasi, elemen itu tetap tidak bisa dipakai keyboard. Solusi paling sederhana: pakai<button>. - Merasa aman setelah skor Lighthouse 100. Skor itu hanya mencakup cek otomatis. Uji dengan Tab, zoom 200%, dan screen reader.
Latihan
- Mudah, Perbaiki:
<img src="logo.png">(logo yang juga link ke beranda) dan<input placeholder="Nama lengkap">. - Sedang, Buat tombol menu hamburger yang aksesibel: tombol dengan nama "Menu",
aria-expandedyang berubah saat diklik, dan daftar menu yang tampil/sembunyi. - Menantang, Buat fungsi
rasioKontras(hex1, hex2)di JavaScript yang menghitung rasio kontras WCAG, lalu uji:#767676di atas#ffffffharus 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
- Setiap input butuh
<label>yang terhubung; placeholder bukan label. Error berupa teks, dihubungkan denganaria-describedby. altmenjelaskan informasi atau fungsi gambar; gambar dekoratif pakaialt="".- Kontras minimal 4.5:1 untuk teks biasa, 3:1 untuk teks besar dan komponen UI. Warna tidak boleh jadi satu-satunya penanda.
- Semua fitur harus jalan dengan keyboard, fokus harus terlihat (
:focus-visible), dan target sentuh minimal 24×24 px. - ARIA hanya menambah informasi, bukan perilaku. Pakai seperlunya setelah HTML bawaan.
- Hormati
prefers-reduced-motion, audit dengan Lighthouse/axe, lalu tetap uji manual.