Aksesibilitas dengan Blade dan Tailwind

Template semantik dan formulir aksesibel dengan Blade dan Tailwind.

Daftar isi

Aksesibilitas adalah kemampuan antarmuka digunakan oleh semua orang, termasuk pengguna pembaca layar (screen reader) dan pengguna yang menavigasi hanya dengan papan ketik. Topik ini membahas cara membangun template Blade yang semantik dan formulir yang aksesibel, dengan Tailwind CSS sebagai penyedia gaya. Pembahasannya mencakup elemen semantik, atribut ARIA, yaitu kumpulan atribut HTML yang memperkaya informasi bagi teknologi bantu, utilitas sr-only, dan indikator fokus yang terlihat, dengan verifikasi render nyata pada proyek aplikasi catatan.

Elemen semantik lebih dahulu#

Elemen HTML yang tepat adalah fondasi aksesibilitas. Elemen label, button, nav, main, dan heading h1 sampai h3 membawa peran bawaan yang otomatis diumumkan oleh pembaca layar, sehingga keterangan tambahan tidak diperlukan. Atribut ARIA hanya melengkapi celah yang tidak tercakup elemen bawaan, misalnya mengaitkan pesan error dengan bidang masukannya. Prinsip kerjanya sederhana: pilih elemen asli lebih dahulu, baru tambahkan ARIA untuk hubungan yang tidak dapat dinyatakan oleh elemen biasa.

Template Blade memberikan kendali penuh atas markah yang dikirim ke browser, karena seluruh HTML ditulis secara eksplisit pada template. Kendali ini membuat keputusan aksesibilitas berada di tangan penulis template, bukan tersembunyi di dalam komponen framework. Aturan pertama formulir adalah setiap bidang masukan memiliki label dengan atribut for yang cocok dengan id bidangnya. Pasangan itu membuat klik pada label memfokuskan bidangnya, dan pembaca layar mengumumkan keduanya sebagai satu kesatuan. Aturan pendampingnya adalah memilih type yang tepat pada setiap input, karena pembaca layar mengumumkan bidang type="email" berbeda dari bidang teks biasa.

Formulir catatan dengan label, petunjuk, dan error#

Formulir berikut menambahkan catatan baru dengan dua bidang teks, yaitu judul dan isi, beserta tombol simpannya. Di samping pasangan label dan for, formulir ini menggunakan tiga hubungan ARIA yang dinyatakan langsung pada markahnya.

bash
# menyiapkan view formulir contoh pada proyek aplikasi catatan
cd ~/aplikasi-catatan
mkdir -p resources/views/uji-aksesibilitas
cat > resources/views/uji-aksesibilitas/formulir.blade.php <<'BLADE'
<form method="POST" action="/catatan">
    @csrf

    <label for="judul" class="block text-sm font-medium text-slate-700">Judul</label>
    <input
        id="judul"
        name="judul"
        type="text"
        value="{{ old('judul') }}"
        aria-describedby="judul-petunjuk @error('judul') judul-error @enderror"
        @error('judul') aria-invalid="true" @enderror
        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200"
    >
    <p id="judul-petunjuk" class="mt-1 text-sm text-slate-500">Gunakan judul singkat, maksimal 60 karakter.</p>
    @error('judul')
        <p id="judul-error" role="alert" class="mt-1 text-sm text-red-600">{{ $message }}</p>
    @enderror

    <label for="isi" class="mt-4 block text-sm font-medium text-slate-700">Isi catatan</label>
    <textarea id="isi" name="isi" rows="4" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200">{{ old('isi') }}</textarea>

    <button type="submit" class="mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 focus-visible:ring-2 focus-visible:ring-indigo-400">
        Simpan catatan
    </button>
</form>
BLADE

Hubungan pertama adalah aria-describedby pada bidang judul, yaitu atribut yang merinci elemen yang berisi keterangan tambahan melalui daftar id. Bidang judul selalu dikaitkan dengan petunjuk judul-petunjuk, dan id judul-error ditambahkan hanya ketika error validasi ada, karena directive @error baru mengeluarkan isinya pada kondisi itu. Hubungan kedua adalah aria-invalid="true" yang menandai bidang yang bermasalah. Hubungan ketiga adalah role="alert" pada paragraf error, yang membuat pembaca layar mengumumkan pesannya segera setelah muncul. Nilai bidang juga tidak hilang saat validasi gagal, karena fungsi old('judul') mengembalikan masukan terakhir dari sesi. Kelas Tailwind pada template murni menata tampilan dan tidak mengubah makna apa pun.

Verifikasi render pada dua kondisi#

Template tersebut dirender melalui Tinker pada dua kondisi, yaitu tanpa error dan dengan error pada bidang judul. Variabel $errors diberikan secara eksplisit karena render berlangsung tanpa middleware sesi.

bash
# merender formulir pada kondisi tanpa error
cd ~/aplikasi-catatan
php artisan tinker --execute='
$bag = new Illuminate\Support\ViewErrorBag();
echo view("uji-aksesibilitas.formulir", ["errors" => $bag])->render();
'
text Keluaran
<form method="POST" action="/catatan">
    <input type="hidden" name="_token" value="" autocomplete="off">
    <label for="judul" class="block text-sm font-medium text-slate-700">Judul</label>
    <input
        id="judul"
        name="judul"
        type="text"
        value=""
        aria-describedby="judul-petunjuk "
                class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200"
    >
    <p id="judul-petunjuk" class="mt-1 text-sm text-slate-500">Gunakan judul singkat, maksimal 60 karakter.</p>
    
    <label for="isi" class="mt-4 block text-sm font-medium text-slate-700">Isi catatan</label>
    <textarea id="isi" name="isi" rows="4" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200"></textarea>

    <button type="submit" class="mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 focus-visible:ring-2 focus-visible:ring-indigo-400">
        Simpan catatan
    </button>
</form>

Pada kondisi bersih, aria-describedby hanya memuat judul-petunjuk, dan tidak ada aria-invalid maupun paragraf error. Kondisi kedua disimulasikan dengan menyuntikkan error validasi pada bidang judul.

bash
# merender formulir dengan error validasi pada bidang judul
cd ~/aplikasi-catatan
php artisan tinker --execute='
$bag = new Illuminate\Support\ViewErrorBag();
$bag->put("default", new Illuminate\Support\MessageBag(["judul" => ["Judul wajib diisi."]]));
echo view("uji-aksesibilitas.formulir", ["errors" => $bag])->render();
'
text Keluaran
<form method="POST" action="/catatan">
    <input type="hidden" name="_token" value="" autocomplete="off">
    <label for="judul" class="block text-sm font-medium text-slate-700">Judul</label>
    <input
        id="judul"
        name="judul"
        type="text"
        value=""
        aria-describedby="judul-petunjuk  judul-error "
         aria-invalid="true"         class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200"
    >
    <p id="judul-petunjuk" class="mt-1 text-sm text-slate-500">Gunakan judul singkat, maksimal 60 karakter.</p>
            <p id="judul-error" role="alert" class="mt-1 text-sm text-red-600">Judul wajib diisi.</p>
    
    <label for="isi" class="mt-4 block text-sm font-medium text-slate-700">Isi catatan</label>
    <textarea id="isi" name="isi" rows="4" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200"></textarea>

    <button type="submit" class="mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 focus-visible:ring-2 focus-visible:ring-indigo-400">
        Simpan catatan
    </button>
</form>

Perbandingan kedua keluaran memperlihatkan bahwa seluruh mekanisme bekerja sesuai markah. aria-describedby bertambah menjadi judul-petunjuk judul-error, atribut aria-invalid="true" muncul pada bidang yang bermasalah, dan paragraf error ber-id judul-error dirender dengan role="alert". Pembaca layar menerima hubungan lengkap ini tanpa JavaScript tambahan, dan kelas Tailwind merangkai tampilannya. Pada aplikasi sungguhan, kondisi error terjadi setelah pengalihan kembali dari validasi, sehingga pesan error muncul bersama nilai lama bidangnya tanpa kode tambahan apa pun di controller selain pemanggilan validasi.

Fokus yang terlihat dan utilitas sr-only#

Navigasi papan ketik bergantung pada indikator fokus, yaitu garis yang menandai elemen aktif. Tailwind menyediakan keluarga utilitas focus: untuk gaya saat bidang difokuskan dan focus-visible: yang hanya berlaku untuk fokus dari papan ketik, sehingga pengguna tetap melihat posisinya. Gaya fokus tidak boleh dihilangkan tanpa pengganti, sehingga kelas outline-none hanya diperbolehkan apabila disertai pengganti seperti focus:ring-2.

Utilitas sr-only menyembunyikan teks secara visual tanpa menghapusnya dari pembaca layar. Utilitas ini digunakan ketika tombol hanya menampilkan ikon, sehingga nama tombol tetap terdengar. Demo berikut merangkum ketiga perilakunya: bidang dengan ring fokus, bidang pada kondisi error dengan pesan yang dirujuk aria-describedby, dan tombol ikon dengan label sr-only. Jalankan demo, klik ke dalam bidang masukan, lalu tekan Tab untuk berpindah fokus.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Formulir aksesibel</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <form>
        <label for="demo-judul">Judul</label>
        <input id="demo-judul" name="judul" type="text" aria-describedby="demo-petunjuk" placeholder="Rapat koordinasi tim">
        <p id="demo-petunjuk" class="petunjuk">Gunakan judul singkat, maksimal 60 karakter.</p>

        <label for="demo-error">Judul pada kondisi error</label>
        <input id="demo-error" name="judul" type="text" aria-invalid="true" aria-describedby="demo-pesan">
        <p id="demo-pesan" role="alert" class="pesan">Judul wajib diisi.</p>

        <button type="button" class="tombol-hapus">
            <span aria-hidden="true">&times;</span>
            <span class="sr-only">Hapus catatan</span>
        </button>
    </form>

<style>
/* Gaya dasar demo */
body {
    margin: 0;
    padding: 24px;
    font-family: system-ui, sans-serif;
    background: #f8fafc;
    color: #0f172a;
}

form {
    max-width: 22rem;
}

/* Setara kelas text-sm font-medium text-slate-700 */
label {
    display: block;
    margin-top: 16px;
    font-size: 0.875rem;
    font-weight: 500;
    color: #334155;
}

/* Setara kelas block w-full rounded-md border-slate-300 shadow-sm
   ditambah focus:border-indigo-600 focus:ring-2 focus:ring-indigo-200 */
input {
    display: block;
    box-sizing: border-box;
    width: 100%;
    margin-top: 4px;
    padding: 8px 10px;
    font-size: 0.875rem;
    border: 1px solid #cbd5e1;
    border-radius: 0.375rem;
    background: #ffffff;
    box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}

input:focus {
    border-color: #4f46e5;
    outline: 2px solid #6366f1;
}

input[aria-invalid="true"] {
    border-color: #dc2626;
}

/* Setara kelas mt-1 text-sm text-slate-500 */
.petunjuk {
    margin: 4px 0 0;
    font-size: 0.875rem;
    color: #64748b;
}

/* Setara kelas mt-1 text-sm text-red-600 */
.pesan {
    margin: 4px 0 0;
    font-size: 0.875rem;
    color: #dc2626;
}

/* Setara kelas rounded-md border-slate-300 text-slate-700
   ditambah focus-visible:ring-2 focus-visible:ring-indigo-400 */
.tombol-hapus {
    margin-top: 16px;
    width: 34px;
    height: 34px;
    border: 1px solid #cbd5e1;
    border-radius: 0.375rem;
    background: #ffffff;
    color: #334155;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}

.tombol-hapus:focus-visible {
    outline: 2px solid #818cf8;
    outline-offset: 1px;
}

/* Definisi kelas sr-only milik Tailwind */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

</style></body>
</html>

Definisi CSS pada demo ditulis sepadan dengan kelas Tailwind aslinya, sebagaimana komentar di berkas index.css. Ring fokus berasal dari focus:ring-2 focus:ring-indigo-200, batas merah bidang error dipicu atribut aria-invalid, dan tombol ikon menggunakan focus-visible:ring-2 yang hanya tampil saat navigasi papan ketik.

Kesalahan umum#

Kesalahan pertama adalah mengganti label dengan atribut placeholder. Teks placeholder hilang segera setelah pengguna mengetik, kontrasnya sering di bawah standar, dan sebagian pembaca layar tidak mengumumkannya sebagai nama bidang. Placeholder boleh digunakan sebagai contoh tambahan, sedangkan label tetap wajib ada.

Kesalahan kedua adalah menghapus garis luar fokus dengan outline: none atau kelas outline-none tanpa pengganti. Pengguna papan ketik kehilangan satu-satunya penanda posisi fokus, dan kondisi ini sulit terdeteksi karena pengujian biasanya dilakukan dengan tetikus. Setiap penghapusan garis luar perlu disertai pengganti yang lebih jelas, misalnya ring pada focus: atau focus-visible:.

Kesalahan ketiga adalah menjadikan elemen div atau span sebagai tombol. Elemen tersebut tidak dapat menerima fokus dengan Tab dan tidak diumumkan sebagai tombol, sehingga tidak dapat dioperasikan dengan papan ketik. Elemen button dengan type yang tepat memberikan perilaku fokus, aktivasi, dan pengumuman tanpa usaha tambahan.

Ringkasan#

  • Aksesibilitas formulir dimulai dari elemen semantik, yaitu pasangan label dan for pada setiap bidang, dan ARIA hanya melengkapi hubungan yang tidak dinyatakan elemen biasa.
  • Kombinasi aria-describedby, aria-invalid, dan role="alert" menghubungkan bidang dengan petunjuk dan pesan errornya, dan Blade mengatur kemunculannya melalui directive @error.
  • Utilitas sr-only menyembunyikan teks secara visual tanpa menghapusnya dari pembaca layar, tepat untuk tombol berbasis ikon.
  • Indikator fokus wajib terlihat, dan utilitas focus: atau focus-visible: digunakan sebagai pengganti setiap kali garis luar bawaan dihilangkan.

Referensi#