Handler validasi kita sudah menampilkan pesan galat merah di bawah kolom, dan handler pengiriman sudah menulis status di paragraf. Semua itu bekerja sempurna bagi pengguna yang melihat layar. Tetapi bagi Siti — anggota komunitas baca bersama yang mengandalkan pembaca layar (screen reader) — pesan merah itu tidak ada artinya: pembaca layar tidak tahu bahwa teks di bawah kolom adalah galat untuk kolom itu, dan tidak tahu pula bahwa status pengiriman sudah berubah di tempat lain.

Aksesibilitas form tidak menuntut kerangka kerja baru; yang dibutuhkan adalah beberapa atribut penghubung dari standar ARIA (Accessible Rich Internet Applications). Chapter ini memasangnya satu per satu pada form pesan singkat: id dan for untuk label, aria-describedby untuk pesan galat, aria-invalid untuk status kolom, dan aria-live untuk umpan balik yang muncul sendiri.

Label yang Terhubung: id dan for #

Fondasi paling dasar adalah pasangan id dan for. Setiap kolom diberi id, dan label di depannya merujuk id yang sama melalui atribut for:

html
<form id="form-kontak">
  <label for="pesan-kontak">Pesan</label>
  <textarea id="pesan-kontak" name="pesan" aria-describedby="petunjuk-galat"></textarea>
  <p id="petunjuk-galat"></p>

  <button type="submit">Kirim Pesan</button>
  <p id="status-kirim" aria-live="polite"></p>
</form>

Hubungan ini memberi dua manfaat sekaligus. Pembaca layar menyebutkan label “Pesan” ketika pengguna berpindah ke kolom tersebut — tanpa hubungan ini, kolom hanya dibacakan sebagai “textarea” kosong tanpa konteks. Dan di browser, klik pada label langsung memindahkan fokus ke kolomnya, area sasaran yang lebih ramah daripada klik presisi pada kolom kecil. Hubungannya juga bisa diperiksa dari JavaScript: document.querySelector("label").htmlFor mengembalikan nilai for yang sama.

Sumber nama lain juga sah: aria-label menempelkan nama langsung pada elemen, dan atribut title menambahkan keterangan saat diarahkan. Tetapi label yang terlihat tetap yang paling disarankan — teksnya bisa dibaca semua pengguna, bukan hanya yang membuka alat bantu, dan isinya yang tampil itulah yang dibacakan pembaca layar.

aria-describedby: Pesan Galat yang Menyatu dengan Kolom #

Pesan galat inline ala chapter Form Validation sudah menulis teks di elemen <p id="petunjuk-galat"> — tetapi pembaca layar tidak tahu teks itu milik kolom mana. Atribut aria-describedby pada kolom menghubungkan keduanya: nilainya adalah id elemen yang berisi penjelasan, dan pembaca layar membacakan isinya bersama kolom saat kolom mendapat fokus. Satu kolom boleh merujuk lebih dari satu id — cukup pisahkan dengan spasi.

Karena hubungan ini statis, atribut cukup ditulis sekali di HTML. Yang berubah dinamis hanyalah isinya — dan di sinilah JavaScript masuk.

aria-invalid dan aria-live: Dua Umpan Balik Dinamis #

Empat atribut yang dipakai di chapter ini bisa dirangkum dalam satu tabel:

AtributDitempel padaPerannya
for pada labellabelmenamai kolom untuk pembaca layar
aria-describedbykolommenghubungkan penjelasan/pesan galat dengan kolom
aria-invalidkolommenandai kolom yang isinya gagal pemeriksaan
aria-live="polite"wadah statusmengumumkan perubahan isi tanpa menunggu fokus

Dua atribut terakhir bekerja secara dinamis — nilainya berubah mengikuti keadaan, dan itulah bagian yang ditangani JavaScript. aria-invalid="true" menandai kolom yang isinya gagal pemeriksaan; pembaca layar menyebut kolom itu “tidak sah” saat membacanya. aria-live pada wadah status memberi tahu pembaca layar untuk mengumumkan perubahan isi wadah tanpa menunggu fokus berpindah — pas untuk hasil pengiriman yang muncul sendiri.

// umpan-balik-aksesibel.js — pesan galat yang terbaca screen reader
const form = document.querySelector("#form-kontak");
const pesan = form.querySelector("#pesan-kontak");
const kotakGalat = form.querySelector("#petunjuk-galat");
const status = form.querySelector("#status-kirim");

// satu pintu untuk menampilkan dan membersihkan galat
function tampilkanGalat(teks) {
  kotakGalat.textContent = teks;
  pesan.setAttribute("aria-invalid", teks === "" ? "false" : "true");
}

form.addEventListener("submit", function (event) {
  event.preventDefault();

  if (pesan.value.trim().length < 5) {
    tampilkanGalat("Pesan minimal 5 karakter.");
    console.log("aria-invalid :", pesan.getAttribute("aria-invalid"));
    console.log("pesan galat  :", kotakGalat.textContent);
    return;
  }

  tampilkanGalat("");
  status.textContent = "Pesan Siti terkirim ke Budi.";
  console.log("aria-invalid :", pesan.getAttribute("aria-invalid"));
  console.log("status live  :", status.textContent);
});

Dua submit kita jalankan: pertama dengan textarea kosong, kedua dengan pesan “Bukunya sudah tiba, terima kasih.”.

Output yang Dihasilkan (console browser):

text
aria-invalid : true
pesan galat  : Pesan minimal 5 karakter.
aria-invalid : false
status live  : Pesan Siti terkirim ke Budi.

Bagian-bagian pentingnya:

  1. Baris 8–11 (tampilkanGalat): satu function mengurus dua hal yang selalu berpasangan — isi pesan galat dan status aria-invalid kolomnya. Menyimpan keduanya di satu pintu mencegah keadaan setengah jadi, misalnya pesan sudah dibersihkan tetapi kolom masih ditandai gagal.
  2. Baris 16–21 (jalur gagal): pesan ditulis dengan textContent ke wadah yang sudah dirujuk aria-describedby, dan setAttribute("aria-invalid", "true") menandai kolom. Keluaran aria-invalid : true memperlihatkan atribut yang akan dibacakan pembaca layar.
  3. Baris 23–27 (jalur sukses): tampilkanGalat("") mengosongkan pesan dan mengembalikan tandanya ke false — umpan balik yang berumur harus bisa dicabut. Status pengiriman ditulis ke wadah aria-live="polite", dan pembaca layar akan mengumumkan kalimat itu tanpa pengguna perlu mencarinya.
  4. Baris 20 (return dini): jalur gagal berhenti sebelum status sukses tersentuh, sehingga kedua wadah tidak pernah berisi informasi yang saling bertentangan.

Daftar Periksa Form yang Ramah Semua #

Empat atribut di atas melengkapi prinsip yang sudah kita pegang sejak awal section ini — dan bentuk daftar periksa membuatnya mudah dijalankan ulang pada form berikutnya:

  • id dan for pada setiap pasangan kolom dan label — tanpa ini, kolom tidak punya nama yang bisa dibacakan.
  • aria-describedby merujuk wadah pesan galat, dan pesan selalu dekat dengan kolom yang bermasalah.
  • aria-invalid dicabut ("false") begitu isian diperbaiki — tandanya harus selalu sesuai keadaan terkini.
  • aria-live="polite" pada wadah status untuk hasil yang muncul tanpa diminta, seperti jawaban pengiriman asinkron.

Cara tercepat mengujinya tanpa alat khusus: berpindahlah antar kolom hanya dengan Tab, dan bayangkan setiap pengumuman yang dibacakan pembaca layar. Apakah nama kolom tersampaikan? Apakah pesan galat dibacakan bersama kolomnya? Apakah hasil pengiriman diumumkan walau fokus tidak berpindah? Pertanyaan-pertanyaan itu memetakan langsung ke empat atribut di atas.

Form kini sudah bersuara untuk semua pengguna. Satu sisa pekerjaan penutup yang tersisa: memperlakukan apa pun yang masuk ke form ini dengan aman, karena isian pengguna adalah data yang harus dijaga dari perlakuan keliru.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

umpan-balik-aksesibel.js

Unduh berkas
javascript
// umpan-balik-aksesibel.js — pesan galat yang terbaca screen reader
const form = document.querySelector("#form-kontak");
const pesan = form.querySelector("#pesan-kontak");
const kotakGalat = form.querySelector("#petunjuk-galat");
const status = form.querySelector("#status-kirim");

// satu pintu untuk menampilkan dan membersihkan galat
function tampilkanGalat(teks) {
  kotakGalat.textContent = teks;
  pesan.setAttribute("aria-invalid", teks === "" ? "false" : "true");
}

form.addEventListener("submit", function (event) {
  event.preventDefault();

  if (pesan.value.trim().length < 5) {
    tampilkanGalat("Pesan minimal 5 karakter.");
    console.log("aria-invalid :", pesan.getAttribute("aria-invalid"));
    console.log("pesan galat  :", kotakGalat.textContent);
    return;
  }

  tampilkanGalat("");
  status.textContent = "Pesan Siti terkirim ke Budi.";
  console.log("aria-invalid :", pesan.getAttribute("aria-invalid"));
  console.log("status live  :", status.textContent);
});