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.
In Part 10 you got the accessibility checklist. {Ở Phần 10 con đã có checklist truy cập.} Now the master goes deep, because accessible data visualization is where most teams fail — and where a senior stands out. {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.} A chart that a screen reader announces only as “graphic” is, to a blind user, an empty box. {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 through the bars, toggle the data table, and watch the live region announce values. {Tab qua các cột, bật/tắt bảng dữ liệu, và xem vùng live đọc giá trị.}
Open the full demo {Mở demo đầy đủ}: /tools/svg-a11y-demo/.
The decorative vs meaningful decision {Quyết định trang trí vs có nghĩa}
Every SVG falls into one of two buckets, and the treatment is opposite. {Mọi SVG rơi vào một trong hai nhóm, và cách xử lý ngược nhau.}
- Decorative (an icon beside a text label, a background flourish) → hide it:
aria-hidden="true". Announcing it is noise. {Trang trí → ẩn nó:aria-hidden="true". Đọc nó là nhiễu.} - Meaningful (a chart, a standalone status icon, a logo that conveys info) → name it. {Có nghĩa → đặt tên cho nó.}
Getting this wrong in either direction is the most common SVG a11y bug: noisy decorative icons, or silent meaningful ones. {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.}
Accessible names: role="img" + aria-labelledby {Tên truy cập: role="img" + aria-labelledby}
<title> alone is inconsistently exposed across browsers and acts like a tooltip. {<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.} The robust pattern wires <title> (short name) and <desc> (longer description) to the SVG with role="img" and aria-labelledby: {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" tells AT to treat the whole SVG as a single image with that name, instead of crawling its dozens of shapes. {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ó.}
Accessible charts: the data table is the real answer {Biểu đồ truy cập: bảng dữ liệu mới là câu trả lời thật}
A <desc> summary is good, but a screen-reader user can’t explore a chart through prose. {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.} The senior pattern pairs the visual SVG with a real, semantic <table> of the same data: {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>
Two options: show the table (a “view as table” toggle, great for everyone), or visually hide it with a .sr-only utility while keeping it in the accessibility tree. {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.} The table is the canonical data; the SVG is a visual enhancement on top. {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;
}
Keyboard-focusable shapes + live regions {Hình focus được bằng phím + vùng live}
For an interactive chart, individual shapes should be reachable and announced. {Cho biểu đồ tương tác, từng hình nên tới được và được đọc.} Make each bar focusable and labelled, and mirror the focused value into an aria-live region: {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" announces changes without yanking focus — perfect for “you are now on Q1: 42”. {aria-live="polite" đọc thay đổi mà không giật focus — hoàn hảo cho “bạn đang ở Q1: 42”.} And never remove focus outlines — style them, with :focus-visible. {Và đừng bao giờ bỏ viền focus — hãy style chúng, với :focus-visible.}
Contrast, motion, and color-only meaning {Tương phản, chuyển động, và nghĩa chỉ-bằng-màu}
- Contrast: chart fills, strokes, and labels must meet WCAG contrast against the background. {Tương phản: fill, stroke, nhãn phải đạt tương phản WCAG so với nền.}
- Don’t encode meaning by color alone. Add labels, patterns, or direct labels so red/green isn’t the only signal (color-blind users). {Đừ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.}
- Motion: respect
prefers-reduced-motionfor any animated SVG (Part 6). {Chuyển động: tôn trọngprefers-reduced-motioncho mọi SVG động.}
How to actually test {Cách thực sự kiểm thử}
Reading specs isn’t testing. {Đọc spec không phải kiểm thử.} Do this: {Hãy làm:}
- Tab through it — can you reach every interactive shape, with a visible focus ring? {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?}
- Turn on a screen reader — VoiceOver (⌘+F5 on macOS), NVDA (Windows), or Orca. Listen to what your SVG announces. {Bật screen reader — VoiceOver, NVDA, hoặc Orca. Nghe SVG đọc gì.}
- Run axe DevTools / Lighthouse for automated catches, but trust the manual pass more. {Chạy axe DevTools / Lighthouse để bắt tự động, nhưng tin lượt thủ công hơn.}
- Inspect the accessibility tree in DevTools to see the computed name/role of each node. {Xem cây truy cập trong DevTools để thấy name/role tính được của từng node.}
The master’s warnings {Lời cảnh báo của sư phụ}
<title>≠ accessible name on its own. Wire it withrole="img"+aria-labelledby. {<title>≠ tên truy cập tự thân. Nối bằngrole="img"+aria-labelledby.}- A chart needs a text alternative, ideally a data table — prose summaries aren’t explorable. {Biểu đồ cần bản thay thế dạng chữ, lý tưởng là bảng dữ liệu.}
- Never
outline: nonewithout a replacement. Keyboard users need to see focus. {Đừng bao giờoutline: nonemà không thay thế. Người dùng phím cần thấy focus.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Label an icon button {Gắn nhãn nút icon}: wrap an SVG icon in a
<button>with anaria-labeland a visible:focus-visiblering. {bọc icon SVG trong<button>cóaria-labelvà vòng:focus-visible.} - Chart + table {Biểu đồ + bảng}: take your Part 9 chart and add a
<title>/<desc>and a toggleable data table. {lấy biểu đồ Phần 9 và thêm<title>/<desc>cùng bảng bật/tắt.} - Screen-reader audit {Kiểm tra screen reader}: turn on VoiceOver/NVDA and navigate one of your SVGs; fix whatever sounds wrong. {bật VoiceOver/NVDA và điều hướng một SVG của con; sửa cái nào nghe sai.}
What’s next {Phần tiếp theo}
Your SVG now speaks to everyone. {SVG của con giờ nói với tất cả mọi người.} Next we industrialize delivery. {Tiếp theo ta công nghiệp hoá việc giao hàng.} In Part 17 we build an icon sprite pipeline — turning a folder of .svg files into one cached <symbol> sheet with build tools (svg-sprite / SVGO / a Vite plugin), and the runtime patterns to inject and use it cleanly. {Ở 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.}