Mở đầu — vì sao bài này quan trọng
Nếu bạn đã đi qua 42 bài trước, bạn đã có kiến thức nền về semantic HTML, ARIA, contrast, keyboard, screen reader, form, component… Nhưng có một khoảng cách rất lớn giữa "biết lý thuyết" và "nhìn ra lỗi trong dự án thật". Trong thực tế, khi bạn được giao audit một sản phẩm đang chạy, bạn sẽ không gặp những ví dụ sạch đẹp như trong sách. Bạn gặp một mớ code chồng chất qua nhiều đời developer, mỗi người thêm một chút, và accessibility là thứ bị bỏ quên đầu tiên.
Bài này là bài "cầm dao mổ". Chúng ta sẽ đi qua những lỗi accessibility phổ biến nhất mà bạn gần như chắc chắn sẽ gặp trên bất kỳ website tiếng Việt nào hiện nay — từ trang thương mại điện tử, app ngân hàng, đến landing page startup. Với mỗi lỗi, tôi sẽ chỉ cho bạn: nó trông như thế nào trong code, tại sao nó phá vỡ trải nghiệm của người dùng khuyết tật, và cách sửa chuẩn.
Điểm khác biệt của bài này so với các bài trước: các bài trước dạy bạn cách làm đúng từng chủ đề riêng lẻ. Bài này gom lại thành một danh mục lỗi (bug catalog) — thứ bạn sẽ mở ra tra cứu mỗi khi review code hoặc fix bug. Hãy coi đây là "cẩm nang sửa lỗi nhanh" của bạn. Theo thống kê thường niên của WebAIM (WebAIM Million), hơn 95% trang chủ của 1 triệu website hàng đầu có ít nhất một lỗi WCAG có thể phát hiện tự động — và đại đa số rơi vào đúng nhóm lỗi chúng ta sắp bàn.
Khái niệm cốt lõi
Trước khi liệt kê lỗi, cần hiểu một nguyên tắc: phần lớn bug a11y không phải do developer cố tình, mà do "đường tắt" (shortcut) trong lúc code. Ví dụ dùng <div> thay <button> vì dễ style hơn, bỏ alt vì "ảnh chỉ để trang trí thôi mà", hay đặt màu chữ xám nhạt vì designer thấy "sang". Mỗi shortcut tiết kiệm cho developer vài giây nhưng dựng lên một bức tường với người dùng khuyết tật.
Theo báo cáo WebAIM Million, sáu nhóm lỗi chiếm áp đảo trên toàn web: contrast thấp, thiếu alt text, link không có text rõ ràng, form thiếu label, heading rỗng, và nút/link dùng sai element. Đây chính là "sáu ông lớn" mà bạn phải thuộc lòng. Dưới đây tôi phân tích từng lỗi kèm cách sửa.
Lỗi 1 — Ảnh thiếu alt
Đây là lỗi kinh điển. Screen reader gặp <img> không có alt sẽ đọc URL của file — điều vô nghĩa như "slash uploads gạch dưới 2024 gạch dưới final v3 chấm jpg".
<!-- SAI -->
<img src="/uploads/hero-2024.jpg">
<!-- ĐÚNG: ảnh mang thông tin -->
<img src="/uploads/hero-2024.jpg" alt="Cô gái đang trang điểm bằng cọ nền trước gương">
<!-- ĐÚNG: ảnh trang trí, dùng alt rỗng để screen reader bỏ qua -->
<img src="/uploads/divider-line.png" alt="">
Quy tắc vàng: mọi <img> đều phải có thuộc tính alt. Nếu ảnh mang thông tin, mô tả nội dung. Nếu ảnh chỉ trang trí, dùng alt="" (chuỗi rỗng) để screen reader bỏ qua hoàn toàn. Sai lầm phổ biến là bỏ luôn thuộc tính alt — khác hoàn toàn với alt="".
Lỗi 2 — Contrast (độ tương phản) quá thấp
Chữ xám nhạt trên nền trắng, hoặc chữ trắng trên nền vàng nhạt. WCAG AA yêu cầu tỉ lệ tương phản tối thiểu 4.5:1 với chữ thường và 3:1 với chữ lớn (từ 18.66px bold hoặc 24px trở lên).
/* SAI: #999 trên nền trắng chỉ đạt 2.85:1 */
.price { color: #999999; background: #ffffff; }
/* ĐÚNG: #767676 đạt đúng 4.5:1 */
.price { color: #767676; background: #ffffff; }
Lỗi 3 — Link/nút không có text rõ ràng
Link chỉ chứa icon, hoặc text kiểu "Xem thêm", "Click here" lặp lại 20 lần trên trang. Screen reader liệt kê danh sách link sẽ đọc "Xem thêm, Xem thêm, Xem thêm…" — người dùng không biết mỗi link dẫn đi đâu.
<!-- SAI: chỉ có icon, screen reader không đọc được gì -->
<a href="/gio-hang"><i class="fa fa-cart"></i></a>
<!-- ĐÚNG: thêm nhãn ẩn hoặc aria-label -->
<a href="/gio-hang" aria-label="Giỏ hàng"><i class="fa fa-cart" aria-hidden="true"></i></a>
Lỗi 4 — Form input thiếu label
Input không gắn với <label>. Người dùng screen reader tab vào ô nhập nhưng không biết cần điền gì — "edit text" trống rỗng.
<!-- SAI: placeholder không thay thế được label -->
<input type="email" placeholder="Email của bạn">
<!-- ĐÚNG -->
<label for="email">Email của bạn</label>
<input type="email" id="email">
Nhớ: placeholder biến mất khi người dùng bắt đầu gõ và thường không được screen reader coi là nhãn chính thức. Nó không bao giờ thay thế <label>.
Lỗi 5 — <div> giả làm nút bấm
Đây là lỗi làm hỏng cả keyboard lẫn screen reader cùng lúc.
<!-- SAI: div không focus được bằng Tab, không kích hoạt bằng Enter/Space -->
<div class="btn" onclick="checkout()">Thanh toán</div>
<!-- ĐÚNG -->
<button type="button" onclick="checkout()">Thanh toán</button>
Lỗi 6 — Heading rỗng hoặc sai thứ bậc
<h1></h1> rỗng (thường do developer dùng heading chỉ để lấy cỡ chữ rồi để trống), hoặc nhảy từ <h1> thẳng xuống <h4>. Screen reader dùng heading để điều hướng nhanh, nên cấu trúc sai làm người dùng lạc lối.
Lỗi 7 — Focus indicator bị xóa
/* SAI: xóa outline khiến người dùng keyboard không biết đang ở đâu */
*:focus { outline: none; }
Nếu bạn thấy dòng outline: none mà không có style focus thay thế, đó gần như luôn là một bug.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT thời trang "Cocoshop" và mùa sale 12/12
Cocoshop là một shop thời trang online giả định tại TP.HCM, doanh thu tầm 8 tỷ/năm. Đợt sale 12/12, họ nhận được email khiếu nại từ một khách hàng khiếm thị: "Tôi không thể hoàn tất thanh toán trên website của anh chị". Team dev vào audit và tá hỏa.
Nút "Thêm vào giỏ" được code bằng <div class="add-btn" onclick="addToCart()">. Khách dùng screen reader NVDA không thể Tab tới nút đó, cũng không nghe thấy nó tồn tại. Tệ hơn, tất cả ảnh sản phẩm đều dùng alt="product image" copy-paste hàng loạt — 200 sản phẩm cùng một alt. Và ô "Mã giảm giá" chỉ có placeholder="Nhập mã" không có label.
Bài học rút ra: chỉ ba lỗi (div-làm-nút, alt vô nghĩa, form thiếu label) đã đủ khiến toàn bộ nhóm khách khiếm thị không mua được hàng. Team sửa trong hai ngày: đổi <div> thành <button>, viết script sinh alt từ tên sản phẩm trong database (alt="Đầm maxi hoa nhí màu xanh"), thêm <label> cho mọi input. Kết quả: tỉ lệ hoàn tất giỏ hàng (checkout completion) tăng 4% — không chỉ nhóm khiếm thị mà cả người dùng bình thường cũng hưởng lợi vì các nút giờ hoạt động nhất quán hơn.
Tình huống 2 — Landing page startup fintech và lỗi contrast "sang chảnh"
Một startup ví điện tử ở Hà Nội thuê agency thiết kế landing page rất đẹp: chữ xám #B0B0B0 trên nền trắng, phần giá gói dịch vụ dùng #C0C0C0. Nhìn trên màn hình Macbook của designer thì "tối giản, sang trọng". Nhưng khi CEO mở trên điện thoại ngoài trời nắng, chính ông ấy còn không đọc nổi.
Chạy công cụ kiểm tra, contrast chỉ đạt 1.9:1 — cách xa mức 4.5:1. Đây là lỗi contrast, nhóm lỗi phổ biến số một theo WebAIM. Điều đáng nói: nó ảnh hưởng không chỉ người khiếm thị mà cả người lớn tuổi (thị lực giảm theo tuổi) và bất kỳ ai dùng máy dưới ánh sáng mạnh.
Cách sửa rất đơn giản: đổi màu chữ chính sang #595959 (đạt 7:1, tức mức AAA), giá tiền sang #333. Bài học: contrast không phải chuyện "thẩm mỹ cá nhân" của designer — nó là ngưỡng đo lường được bằng con số. Đừng tranh cãi cảm tính, hãy đưa ra tỉ lệ contrast cụ thể.
Tình huống 3 — Blog tin tức và cái bẫy outline: none
Một trang tin tức có mục bình luận. Developer front-end mới vào, thấy khi click vào các nút thì có "viền xanh xấu xí" nên thêm dòng CSS reset toàn cục: *:focus { outline: none; }. Trang trông "sạch" hơn hẳn.
Ba tháng sau, một sinh viên bị chấn thương tay chỉ dùng được bàn phím phản hồi rằng cậu không thể điều hướng trang: nhấn Tab liên tục nhưng không biết con trỏ đang ở nút nào vì không có dấu hiệu focus nào cả. Đây là lỗi xóa focus indicator — một trong những bug âm thầm nhưng nghiêm trọng nhất, vì nó không hiện ra khi bạn test bằng chuột.
Cách sửa: không bao giờ xóa outline mà không thay thế. Thay outline: none bằng một style focus rõ ràng:
:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Dùng :focus-visible để chỉ hiện viền khi người dùng dùng bàn phím, còn khi click chuột thì không — vừa đẹp cho người dùng chuột, vừa an toàn cho người dùng keyboard. Bài học: mỗi khi bạn thấy outline: none trong codebase, hãy coi đó là "cờ đỏ" cần điều tra ngay.
Hướng dẫn từng bước
Đây là quy trình săn và sửa bug a11y mà tôi khuyên bạn áp dụng khi nhận một trang cần audit:
Bước 1 — Quét tự động để bắt "quả thấp". Mở trang cần kiểm tra, chạy một extension như axe DevTools hoặc WAVE (bạn đã học ở Bài 37). Công cụ này bắt được khoảng 30–40% lỗi: thiếu alt, contrast thấp, form thiếu label, heading rỗng. Đây là những lỗi "hái quả trên cành thấp" — dễ tìm, dễ sửa. Ghi lại danh sách lỗi.
Bước 2 — Kiểm tra bàn phím. Cất chuột đi. Nhấn Tab từ đầu trang. Với mỗi phần tử tương tác, tự hỏi: (a) Tôi có Tab tới được không? (b) Tôi có nhìn thấy con trỏ focus đang ở đâu không? (c) Nhấn Enter/Space có kích hoạt được không? Nếu một nút "Thanh toán" không Tab tới được, gần như chắc chắn nó là <div> trá hình.
Bước 3 — Kiểm tra bằng screen reader. Bật NVDA (Windows) hoặc VoiceOver (Mac), nhắm mắt lại và dùng trang. Bạn sẽ nghe ngay những chỗ đọc "link", "button" trống rỗng, hoặc đọc tên file ảnh. Đây là cách phát hiện lỗi mà công cụ tự động bỏ sót.
Bước 4 — Phân loại theo mức độ ưu tiên. Không phải lỗi nào cũng như nhau. Sắp xếp theo tác động: lỗi chặn hoàn toàn (blocker — như nút thanh toán không dùng được) sửa trước; lỗi gây khó chịu nhưng vẫn dùng được (như alt hơi mơ hồ) sửa sau.
Bước 5 — Sửa từ gốc, không vá tạm. Khi gặp <div class="btn">, đừng thêm role="button" và tabindex="0" rồi tự viết xử lý phím — hãy đổi thẳng thành <button>. Element gốc (native) đã có sẵn mọi hành vi accessibility. Chỉ dùng ARIA khi HTML thuần không đủ (bạn đã học nguyên tắc này ở Bài 8).
Bước 6 — Kiểm tra lại (re-test). Sau khi sửa, chạy lại cả ba lớp kiểm tra ở bước 1–3 để chắc chắn không phát sinh lỗi mới.
Lỗi thường gặp & mẹo
Nhầm alt="" với việc bỏ alt. alt="" bảo screen reader "bỏ qua ảnh này, nó chỉ trang trí" — hoàn toàn hợp lệ. Bỏ hẳn thuộc tính alt khiến screen reader phải đoán và đọc URL. Hai thứ này khác nhau hoàn toàn.
Dùng aria-label để "vá" cho <div> thay vì đổi element. Nhiều bạn thêm role="button" + aria-label + tabindex="0" cho <div> rồi nghĩ đã xong. Nhưng bạn vẫn phải tự xử lý phím Enter và Space, và rất dễ quên. Đổi sang <button> là xong trong một dòng.
Sửa contrast bằng cảm tính. Đừng nhìn bằng mắt rồi phán "chắc ổn". Luôn dùng công cụ đo (Chrome DevTools có sẵn màu contrast ratio ngay trong tab Styles). Con số 4.5:1 là ngưỡng bắt buộc, không phải gợi ý.
Chỉ dựa vào công cụ tự động. Công cụ như axe chỉ bắt được 30–40% lỗi. Nó không biết alt="hình ảnh" là vô nghĩa (nó chỉ thấy "à, có alt rồi"). Nó không biết thứ tự Tab có logic hay không. Kiểm tra thủ công là bắt buộc.
Xóa outline vì "xấu". Lỗi này lặp đi lặp lại. Mẹo: đặt :focus-visible với style đẹp thay vì xóa. Vừa thẩm mỹ vừa accessible.
Placeholder làm label. Placeholder biến mất khi gõ, contrast thường thấp, và không phải screen reader nào cũng đọc. Luôn có <label> thật.
Mẹo review nhanh: khi review pull request của đồng nghiệp, hãy Ctrl+F tìm ba chuỗi này trong code: <div onclick, outline: none, và placeholder= (kiểm tra xem có label kèm không). Ba từ khóa này bắt được phần lớn bug a11y phổ biến chỉ trong vài phút.
Bài tập thực hành
- Săn bug trên trang thật. Chọn một website tiếng Việt bạn hay dùng (một sàn TMĐT, một trang báo). Chạy extension axe DevTools hoặc WAVE, chụp lại màn hình danh sách lỗi. Đếm xem có bao nhiêu lỗi rơi vào "sáu ông lớn" (contrast, alt, link, form label, heading, sai element).
- Fix một trang lỗi có sẵn. Tạo một file HTML nhỏ chứa cố ý cả 7 lỗi trong bài (div-làm-nút, ảnh thiếu alt, contrast thấp, input thiếu label, link chỉ có icon, heading rỗng,
outline: none). Sau đó sửa từng lỗi và ghi chú bên cạnh mỗi dòng sửa: lỗi gì, sửa thế nào, vì sao.
- Test bằng bàn phím. Lấy chính trang bạn vừa fix ở bài 2, cất chuột, dùng hoàn toàn bằng Tab/Enter/Space. Viết ra một checklist: mỗi phần tử tương tác có (a) Tab tới được, (b) thấy focus, (c) kích hoạt được không.
- Viết mini bug report. Chọn một lỗi bạn tìm được ở bài tập 1, viết một báo cáo bug ngắn theo mẫu: Mô tả lỗi → Ảnh hưởng đến nhóm người dùng nào → Mức độ nghiêm trọng → Đề xuất cách sửa kèm code. Đây chính là kỹ năng bạn sẽ dùng hằng ngày khi làm a11y chuyên nghiệp.
Tóm tắt
Bài này gói lại thành một danh mục lỗi a11y phổ biến nhất — thứ bạn sẽ mở ra tra cứu suốt sự nghiệp. Hãy nhớ "sáu ông lớn" cộng thêm hai lỗi keyboard: (1) ảnh thiếu alt, (2) contrast thấp dưới 4.5:1, (3) link/nút không có text rõ ràng, (4) form input thiếu <label>, (5) <div> giả làm nút bấm, (6) heading rỗng hoặc sai thứ bậc, (7) xóa focus indicator, và cạm bẫy placeholder thay label.
Nguyên tắc cốt lõi xuyên suốt: phần lớn bug đến từ "đường tắt" của developer, và cách sửa tốt nhất luôn là quay về HTML gốc thay vì vá bằng ARIA. Quy trình săn bug ba lớp — quét tự động, test bàn phím, test screen reader — sẽ giúp bạn bắt được cả lỗi máy tìm ra lẫn lỗi chỉ con người mới thấy. Và mẹo review nhanh với ba từ khóa <div onclick, outline: none, placeholder= sẽ giúp bạn chặn bug ngay từ khâu code review, trước khi nó lên production và chặn đường một người dùng thật.