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
Let's register a Viblo Account to get more interesting posts.