jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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"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-motion for any animated SVG (Part 6). {Chuyển động: tôn trọng prefers-reduced-motion cho 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:}

  1. 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?}
  2. 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ì.}
  3. 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.}
  4. 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 with role="img" + aria-labelledby. {<title> ≠ tên truy cập tự thân. Nối bằng role="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: none without a replacement. Keyboard users need to see focus. {Đừng bao giờ outline: none mà 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}

  1. Label an icon button {Gắn nhãn nút icon}: wrap an SVG icon in a <button> with an aria-label and a visible :focus-visible ring. {bọc icon SVG trong <button>aria-label và vòng :focus-visible.}
  2. 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.}
  3. 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.}