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.