ARIA (Accessible Rich Internet Applications) adalah atribut dari W3C yang mengubah apa yang tertulis di pohon aksesibilitas: role (apa elemen ini), state (keadaannya sekarang), dan property (hubungannya dengan elemen lain). ARIA gak mengubah perilaku sama sekali. Bayangin gudang Bu Sri yang penuh kardus. Stiker "Selimut" di sebuah kardus gak memasukkan selimut ke dalamnya; stiker cuma memberi tahu orang yang gak bisa melihat isinya. Stiker yang benar sangat membantu, stiker yang salah membuat orang mengambil kardus yang salah. ARIA adalah stiker itu, dan screen reader adalah orang yang cuma bisa membaca stikernya.
🧪 Tebakan cepat: Tim Bu Sri membuat tab foto kamar dari
<h2 role="tab">Kamar</h2>. Di pohon aksesibilitas Chromium, heading "Kamar" masih ada? Dan bisakah pengguna keyboard mencapai tab itu dengan Tab? Jawabannya ada di bagian "Role yang salah".
Empat aturan ARIA
Catatan W3C "Using ARIA" menulis empat aturan. Dokumennya sekarang berstatus Discontinued Draft (Februari 2026): aturannya disimpan sebagai rujukan, dan panduan yang aktif adalah ARIA Authoring Practices Guide (APG).
- Pakai elemen HTML asli kalau ada.
<button>lebih baik dari<div role="button">, karena<button>sudah bisa difokus dan ditekan. - Jangan mengubah makna elemen asli, kecuali terpaksa. Jangan
<h2 role="tab">. - Semua kontrol ARIA harus bisa dipakai dengan keyboard. Role cuma janji; perilakunya kamu yang menulis.
- Jangan pasang
role="presentation"atauaria-hidden="true"di elemen yang bisa difokus. Fokus mendarat di sesuatu yang gak ada di pohon.
Aturan pertama menjelaskan kenapa <nav role="navigation"> redundant: <nav> sudah membawa role navigation. Sampai 2023, area pencarian gak punya elemen sendiri, jadi <div role="search"> mengisi celah itu. Sejak 2023 HTML punya <search>; Chrome, Safari, dan Firefox versi yang kami uji sudah mengenalnya, dan Chromium 147 memberinya role search di pohon. <div role="search"> tetap benar untuk browser lama.
Role, state, dan property
- Role: jenis elemen.
tab,tablist,tabpanel,button,status. - State: berubah saat dipakai.
aria-selected,aria-expanded,aria-pressed. - Property: hubungan atau sifat yang jarang berubah.
aria-controls(mengendalikan elemen mana),aria-labelledby(namanya diambil dari elemen mana).
Di halaman Homestay Kabut ada empat bagian yang butuh lebih dari HTML biasa:
| Bagian | Role | State dan property |
|---|---|---|
| Tab foto Kamar, Kamar mandi, Teras | tablist, tab, tabpanel |
aria-selected, aria-controls, aria-labelledby |
| Tombol "Fasilitas kamar" yang membuka daftar | button (asli) |
aria-expanded, aria-controls |
| Tombol "Tambah sarapan" | button (asli) |
aria-pressed |
| Total harga | status |
dibacakan saat berubah |
Tab foto kamar: tablist, tab, tabpanel
<div role="tablist" aria-label="Foto Kamar Teras">
<button role="tab" id="tab-kamar" aria-selected="true" aria-controls="panel-kamar">Kamar</button>
<button role="tab" id="tab-mandi" aria-selected="false" aria-controls="panel-mandi" tabindex="-1">Kamar mandi</button>
<button role="tab" id="tab-teras" aria-selected="false" aria-controls="panel-teras" tabindex="-1">Teras</button>
</div>
<div role="tabpanel" id="panel-kamar" aria-labelledby="tab-kamar" tabindex="0">
<img src="kamar.svg" alt="Ranjang ganda di depan jendela besar">
</div>
<div role="tabpanel" id="panel-mandi" aria-labelledby="tab-mandi" tabindex="0" hidden>...</div>
<div role="tabpanel" id="panel-teras" aria-labelledby="tab-teras" tabindex="0" hidden>...</div>
<script>
const tabs = document.querySelectorAll('[role="tab"]');
for (const tab of tabs) {
tab.addEventListener('click', () => {
for (const t of tabs) {
const on = t === tab;
t.setAttribute('aria-selected', String(on)); // state ikut diperbarui
t.tabIndex = on ? 0 : -1;
document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
}
});
}
</script>
role="tablist"membungkus tab-tabnya, danaria-labelmemberinya nama.aria-selected="true"menandai tab yang aktif. Script-nya wajib memperbarui nilai ini setiap kali tab berganti; ARIA gak memperbarui dirinya sendiri.aria-controlsdi tab menunjukidpanelnya;aria-labelledbydi panel menunjukidtab-nya, jadi panel bernama "Kamar".- Panel yang gak aktif diberi
hidden, jadi hilang dari tampilan dan dari pohon. tabindexdan tombol panah dibahas di pelajaran berikutnya.
Kami membaca pohonnya dari Chromium 147 (Chrome DevTools Protocol, Accessibility.getFullAXTree):
| Versi | Yang diterima screen reader |
|---|---|
Tiga <button> biasa, yang aktif cuma diberi warna |
button "Kamar", button "Kamar mandi", button "Teras": gak ada yang tahu foto mana yang tampil |
| Pola di atas | tablist "Foto Kamar Teras" berisi tab "Kamar" selected=true, tab "Kamar mandi" selected=false, tab "Teras" selected=false, lalu tabpanel "Kamar" |
aria-expanded dan aria-pressed
<button aria-expanded="false" aria-controls="fasilitas">Fasilitas kamar</button>
<ul id="fasilitas" hidden>
<li>Air panas</li>
<li>Selimut tebal</li>
</ul>
<button class="sarapan" aria-pressed="false">Tambah sarapan</button>
aria-expandedmemberi tahu apakah yang dikendalikan tombol sedang terbuka. Chromium:button "Fasilitas kamar" expanded=false, laluexpanded=truesetelah script mengubahnya saat diklik.- Aturan pertama lagi:
<details><summary>Fasilitas kamar</summary>...</details>memberi hal yang sama tanpa JavaScript. Chromium menulisnyaDisclosureTriangle "Fasilitas kamar" expanded=false, dan browser sendiri yang memperbarui state-nya. aria-pressedmengubah tombol jadi toggle button, tombol yang tetap tertekan atau gak. Chromium:button "Tambah sarapan" pressed=false. APG (pola Button) menegaskan labelnya jangan berganti saat state-nya berubah: tetap "Tambah sarapan", state-nya yang berganti. Kalau labelnya berganti jadi "Hapus sarapan", jangan pakaiaria-pressed.
aria-live: kabar tanpa memindahkan fokus
Saat Pak Hadi menambah sarapan, total berubah di bagian lain halaman. Fokusnya tetap di tombol, jadi tanpa live region perubahan itu gak dibacakan.
<p class="total" role="status">Total 2 malam: <b>Rp 700.000</b></p>
| Markup | Di pohon Chromium 147 | Yang dibacakan saat berubah |
|---|---|---|
role="status" |
status live=polite atomic=true |
seluruh kalimat, setelah screen reader selesai bicara |
aria-live="polite" di <p> |
live=polite atomic=false |
bisa cuma bagian yang berubah |
role="alert" |
alert live=assertive atomic=true |
seluruh kalimat, memotong yang sedang dibacakan |
aria-atomic="true" artinya seluruh region dibacakan, bukan cuma potongan yang berubah. Toast "Pesanan terkirim" yang muncul tanpa memindahkan fokus juga diberi role="status"; role="alert" disimpan untuk error yang harus segera ditangani. Region-nya harus sudah ada sebelum teksnya berubah.
Role yang salah
Role yang salah lebih buruk dari gak ada role: dia menghapus makna yang benar. Kami mencobanya di Chromium 147:
| Markup | Di pohon | Akibatnya |
|---|---|---|
<h2 role="tab">Kamar</h2> |
tab "Kamar", gak focusable |
heading hilang dari daftar heading, dan Tab melewati tab itu |
<button role="tabs"> (salah ketik) |
button "Kamar" |
role yang gak dikenal diabaikan; jadi tombol biasa |
<button role="heading" aria-level="2"> |
heading "Kamar" level 2, tetap focusable |
screen reader menyebutnya heading, padahal bisa diklik |
<button role="presentation"> |
button "Teras" |
aturan keempat: browser menolak presentation di elemen yang bisa difokus |
Jadi jawaban tebakannya: gak dua-duanya. Heading "Kamar" diganti tab "Kamar", jadi pengguna yang melompat lewat daftar heading gak menemukannya lagi, dan karena <h2> gak bisa difokus, Tab melewatinya. Itu jawaban tebakannya. Itulah aturan kedua: tab dibuat dari <button role="tab">.
⚠️ Jebakan yang sering ketemu
- ARIA sebelum HTML.
<div role="button">tanpatabindexdan kode keyboard cuma pura-pura jadi tombol. Pakai<button>,<details>,<search>. - State yang gak diperbarui.
aria-expanded="false"yang tetap false saat daftarnya terbuka membuat screen reader bilang "tertutup". Ubah state di kode yang sama yang mengubah tampilan. aria-controlsatauaria-labelledbykeidyang gak ada. Hubungannya putus tanpa pesan error. Cekid-nya sama persis.- Mengganti label sekaligus memakai
aria-pressed. Pilih salah satu: label tetap denganaria-pressed, atau label yang berganti tanpaaria-pressed. role="alert"untuk semua kabar. Alert memotong apa pun. Kabar biasa cukuprole="status".
TL;DR: ARIA mengubah pohon aksesibilitas (role, state, property), bukan perilaku. Empat aturan (W3C "Using ARIA"): elemen HTML asli dulu; jangan ubah makna elemen asli (<h2 role="tab"> menghapus heading-nya); kontrol ARIA harus bisa dipakai dengan keyboard; jangan presentation atau aria-hidden di elemen yang bisa difokus. <nav> sudah navigation, jadi role="navigation" redundant; role="search" mengisi celah sampai HTML punya <search> (2023). Tab foto: tablist berisi tab dengan aria-selected dan aria-controls, panel tabpanel dengan aria-labelledby. Disclosure: aria-expanded (atau <details> tanpa JavaScript). Toggle: aria-pressed dengan label yang gak berganti. Kabar tanpa memindahkan fokus: role="status" (polite, seluruh kalimat), error mendesak role="alert". State harus diperbarui oleh kodemu, dan hasilnya bisa dicek di pohon aksesibilitas DevTools.