SVG from Zero to Senior · Part 16 — Accessibility Deep-Dive
Go past title/desc: accessible names with role=img and aria-labelledby, the decorative-vs-meaningful decision, keyboard-focusable shapes, aria-live readouts, accessible charts backed by a real data table, and how to test. With a live demo.
Ở Phần 10 con đã có checklist truy cập. Giờ sư phụ đào sâu, vì trực quan dữ liệu truy cập được là chỗ đa số đội ngũ thất bại — và là chỗ một senior nổi bật. Một biểu đồ mà screen reader chỉ đọc là “graphic” thì, với người dùng khiếm thị, là một cái hộp rỗng.
Tab qua các cột, bật/tắt bảng dữ liệu, và xem vùng live đọc giá trị.
Mở demo đầy đủ:
Quyết định trang trí vs có nghĩa
Mọi SVG rơi vào một trong hai nhóm, và cách xử lý ngược nhau.
- Trang trí → ẩn nó:
aria-hidden="true". Đọc nó là nhiễu. - Có nghĩa → đặt tên cho nó.
Sai theo chiều nào cũng là lỗi a11y SVG phổ biến nhất: icon trang trí ồn ào, hoặc icon có nghĩa câm lặng.
Tên truy cập: role="img" + aria-labelledby
<title> đứng một mình được phơi bày không nhất quán giữa các trình duyệt và hoạt động như tooltip. Mẫu chắc chắn nối <title> (tên ngắn) và <desc> (mô tả dài) vào SVG bằng role="img" và aria-labelledby:
<svg role="img" aria-labelledby="title desc" viewBox="0 0 300 180">
<title id="title">Quarterly revenue 2025</title>
<desc id="desc">Bar chart: Q1 42, Q2 78, Q3 60, Q4 90 (thousands).</desc>
<!-- shapes -->
</svg>
role="img" bảo công nghệ hỗ trợ coi cả SVG như một ảnh đơn với tên đó, thay vì bò qua hàng tá hình của nó.
Biểu đồ truy cập: bảng dữ liệu mới là câu trả lời thật
Một tóm tắt <desc> thì tốt, nhưng người dùng screen reader không thể khám phá biểu đồ qua văn xuôi. Mẫu senior ghép SVG thị giác với một <table> ngữ nghĩa thật của cùng dữ liệu:
<table>
<caption>Quarterly revenue 2025 (thousands)</caption>
<thead><tr><th scope="col">Quarter</th><th scope="col">Revenue</th></tr></thead>
<tbody>
<tr><th scope="row">Q1</th><td>42</td></tr>
<!-- … -->
</tbody>
</table>
Hai lựa chọn: hiện bảng (nút “xem dạng bảng”, tốt cho mọi người), hoặc ẩn bằng .sr-only mà vẫn giữ trong cây truy cập. Bảng là dữ liệu chuẩn; SVG là phần tăng cường thị giác bên trên.
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
Hình focus được bằng phím + vùng live
Cho biểu đồ tương tác, từng hình nên tới được và được đọc. Làm mỗi cột focus được và có nhãn, và phản chiếu giá trị đang focus vào một vùng aria-live:
<rect tabindex="0" role="img" aria-label="Q1: 42 thousand" />
<p role="status" aria-live="polite" id="live"></p>
bar.addEventListener('focus', () => {
document.getElementById('live').textContent = bar.getAttribute('aria-label');
});
aria-live="polite" đọc thay đổi mà không giật focus — hoàn hảo cho “bạn đang ở Q1: 42”. Và đừng bao giờ bỏ viền focus — hãy style chúng, với :focus-visible.
Tương phản, chuyển động, và nghĩa chỉ-bằng-màu
- Tương phản: fill, stroke, nhãn phải đạt tương phản WCAG so với nền.
- Đừng mã hoá nghĩa chỉ bằng màu. Thêm nhãn, pattern, hoặc nhãn trực tiếp để đỏ/xanh không phải tín hiệu duy nhất.
- Chuyển động: tôn trọng
prefers-reduced-motioncho mọi SVG động.
Cách thực sự kiểm thử
Đọc spec không phải kiểm thử. Hãy làm:
- Tab qua nó — con tới được mọi hình tương tác, có vòng focus nhìn thấy không?
- Bật screen reader — VoiceOver, NVDA, hoặc Orca. Nghe SVG đọc gì.
- Chạy axe DevTools / Lighthouse để bắt tự động, nhưng tin lượt thủ công hơn.
- Xem cây truy cập trong DevTools để thấy name/role tính được của từng node.
Lời cảnh báo của sư phụ
<title>≠ tên truy cập tự thân. Nối bằngrole="img"+aria-labelledby.- Biểu đồ cần bản thay thế dạng chữ, lý tưởng là bảng dữ liệu.
- Đừng bao giờ
outline: nonemà không thay thế. Người dùng phím cần thấy focus.
Luyện tập, không thì coi như chưa học
- Gắn nhãn nút icon: bọc icon SVG trong
<button>cóaria-labelvà vòng:focus-visible. - Biểu đồ + bảng: lấy biểu đồ Phần 9 và thêm
<title>/<desc>cùng bảng bật/tắt. - Kiểm tra screen reader: bật VoiceOver/NVDA và điều hướng một SVG của con; sửa cái nào nghe sai.
Phần tiếp theo
SVG của con giờ nói với tất cả mọi người. Tiếp theo ta công nghiệp hoá việc giao hàng. Ở Phần 17 ta dựng một pipeline sprite icon — biến một thư mục file .svg thành một sheet <symbol> được cache bằng công cụ build (svg-sprite / SVGO / plugin Vite), và các mẫu runtime để inject và dùng nó gọn gàng.