0

Đừng phá hỏng Web: Hướng dẫn Accessibility với ARIA cho lập trình viên

Lỗi có thể tốn hàng triệu đô

<!-- This looks fine, works fine, but breaks for 1 billion users -->
<div class="btn" onclick="submit()">
  Submit Form
</div>

<!-- This actually works for everyone -->
<button type="submit">Submit Form</button>

Sự thật phũ phàng: 96,3% trang web không vượt qua các bài kiểm tra accessibility cơ bản. Các công ty như Target (bị phạt 6 triệu USD), Domino’s (thua kiện tại Tòa Tối Cao) và Netflix (bị phạt 755.000 USD) đã phải học bài học này theo cách rất tốn kém.

ARIA - Tóm tắt cho lập trình viên bận rộn

ARIA = Accessible Rich Internet Applications

Đây là API giúp kết nối các thành phần tuỳ chỉnh của bạn với công nghệ hỗ trợ (trình đọc màn hình, v.v.).

Chỉ có 3 khái niệm cốt lõi:

  • Role – Nó là cái gì?
role="button"
  • Property – Tính chất của nó?
aria-required="true"
  • State – Trạng thái hiện tại?
aria-expanded="false"

Kiểm tra Accessibility nhanh trong 2 phút

1. Kiểm tra bằng Terminal

npm install -g @axe-core/cli
axe https://yoursite.com

2. Kiểm tra thủ công (hãy làm ngay)

  • Dùng phím Tab để di chuyển qua trang (không dùng chuột)
  • Bật chế độ tương phản cao (High Contrast Mode)
  • Phóng to trang lên 200%
  • Dùng VoiceOver (Mac: Cmd+F5) hoặc NVDA (Windows, miễn phí)

Nếu bất kỳ bước nào thất bại, trang của bạn đang gây khó khăn cho hàng triệu người dùng.

Mẫu ARIA thiết yếu (Sao chép – dán – sử dụng)

Phần tử tương tác tuỳ chỉnh

<!-- Don't reinvent the wheel -->
<button>Native Button</button>

<!-- But if you must... -->
<div role="button" 
     tabindex="0"
     aria-label="Close dialog"
     onKeyPress="handleEnterSpace(event)"
     onClick="handleClick()">
  ×
</div>

<script>
function handleEnterSpace(e) {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    handleClick();
  }
}
</script>

Form Validation hữu ích

<!-- Before: Silent failures -->
<input type="email" required>
<span class="error hidden">Invalid email</span>

<!-- After: Accessible feedback -->
<label for="email">Email Address</label>
<input id="email" 
       type="email" 
       aria-required="true"
       aria-invalid="false"
       aria-describedby="email-error">
<div id="email-error" 
     role="alert" 
     aria-live="assertive"
     class="error hidden">
  Invalid email format
</div>

Dynamic Content Updates

<!-- Status container -->
<div id="status" role="status" aria-live="polite"></div>

<script>
// This announces to screen readers
document.getElementById('status').textContent = 'Changes saved!';

// For urgent updates
document.getElementById('status').setAttribute('aria-live', 'assertive');
</script>

Thành phần điều hướng

<!-- Accessible dropdown -->
<nav>
  <button aria-expanded="false" 
          aria-controls="nav-menu"
          aria-haspopup="true">
    Menu
  </button>
  <ul id="nav-menu" hidden>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

React + ARIA (Cách làm đúng)

import { useState, useRef, useEffect } from 'react';

function AccessibleModal({ isOpen, onClose, title, children }) {
  const modalRef = useRef(null);
  const previousFocus = useRef(null);

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement;
      modalRef.current?.focus();
    } else {
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') {
      onClose();
    }
  };

  if (!isOpen) return null;

  return (
    <div 
      className="modal-overlay"
      onClick={onClose}
    >
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        onKeyDown={handleKeyDown}
        onClick={(e) => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

// Usage
<AccessibleModal 
  isOpen={showModal} 
  onClose={() => setShowModal(false)}
  title="Confirm Action"
>
  <p>Are you sure you want to delete this item?</p>
</AccessibleModal>

Debug ARIA (Script cho Dev Tools)

Tìm input không có nhãn

console.log('Unlabeled inputs:', 
  Array.from(document.querySelectorAll('input, textarea, select'))
    .filter(el => !el.labels?.length && 
                  !el.getAttribute('aria-label') && 
                  !el.getAttribute('aria-labelledby'))
);

Kiểm tra tabindex không hợp lý

console.log('Positive tabindex (avoid these):', 
  Array.from(document.querySelectorAll('[tabindex]'))
    .filter(el => el.tabIndex > 0)
);

Tìm phần tử tương tác thiếu role

console.log('Interactive divs/spans missing roles:',
  Array.from(document.querySelectorAll('div[onclick], span[onclick]'))
    .filter(el => !el.getAttribute('role'))
);

7 Lỗi ARIA thường gặp

1. Role dư thừa

<!-- ❌ Redundant -->
<button role="button">Click me</button>

<!-- ✅ Native semantics -->
<button>Click me</button>

2. Thiếu hỗ trợ bàn phím

<!-- ❌ Mouse-only -->
<div role="button" onclick="handleClick()">Submit</div>

<!-- ✅ Keyboard accessible -->
<div role="button" 
     tabindex="0" 
     onclick="handleClick()"
     onkeydown="handleKeyPress(event)">Submit</div>

3. Quản lý focus sai

<!-- ❌ Focus disappears -->
<button onclick="this.remove()">Delete</button>

<!-- ✅ Focus moves logically -->
<button onclick="deleteAndFocus()">Delete</button>

4. Lạm dụng aria-label

<!-- ❌ Unnecessary -->
<h1 aria-label="Page Title">Page Title</h1>

<!-- ✅ Only when needed -->
<button aria-label="Close dialog">×</button>

Công cụ thiết yếu

Extensions nên cài

  • axe DevTools – Quét accessibility tự động
  • WAVE – Đánh giá trực quan
  • Lighthouse – Tích hợp sẵn trong Chrome DevTools

Trình đọc màn hình miễn phí

  • NVDA (Windows) – Tải tại nvaccess.org
  • VoiceOver (Mac) – Tích hợp sẵn, bật bằng Cmd+F5
  • TalkBack (Android) – Tích hợp sẵn

Các lệnh nhanh

Kiểm tra accessibility bằng Lighthouse

lighthouse https://yoursite.com --only-categories=accessibility

Kiểm tra độ tương phản màu (nếu đã cài node)

npx @adobe/leonardo-contrast-colors --bg "#ffffff" --colors "#0066cc"

Checklist kiểm tra trước khi Merge Pull Request

  • Tab navigation hoạt động không cần chuột
  • Có hiển thị focus rõ ràng
  • Trình đọc màn hình đọc nội dung chính xác
  • Màu sắc đạt tiêu chuẩn WCAG AA (4.5:1)
  • Lỗi form được thông báo
  • Nội dung động được thông báo
  • Thành phần tuỳ chỉnh có role phù hợp
  • Skip links hoạt động

Trải nghiệm của bạn?

Hãy để lại bình luận:

  • Bạn gặp khó khăn gì với accessibility?
  • Bạn tin dùng công cụ nào nhất?
  • Có câu chuyện kinh hoàng nào từ các cuộc audit accessibility?

Hãy cùng nhau xây dựng web dễ dùng cho mọi người, từng component một.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí