Mở đầu — vì sao bài này quan trọng
Hãy hình dung bạn bước vào một tòa nhà văn phòng, nhưng tất cả các cánh cửa đều giống hệt nhau: không biển tên, không ký hiệu phòng vệ sinh, không mũi tên chỉ lối thoát hiểm, không phân biệt đâu là cầu thang và đâu là nhà kho. Người sáng mắt vẫn có thể mò mẫm đoán được đôi chút, nhưng một người khiếm thị dùng gậy dò đường sẽ hoàn toàn lạc lối. Đó chính xác là cảm giác của một người dùng trình đọc màn hình khi truy cập một website được xây dựng toàn bằng thẻ <div> và <span>.
Semantic HTML — HTML ngữ nghĩa — là nền tảng số một của accessibility. Không phải ARIA, không phải color contrast, không phải keyboard navigation. Tất cả những kỹ thuật đó bạn sẽ học ở các bài sau đều được xây dựng trên nền semantic HTML. Nếu nền móng này sai, mọi thứ chồng lên trên đều lung lay. Có một câu nói kinh điển trong cộng đồng a11y: "The first rule of ARIA is: don't use ARIA" — quy tắc đầu tiên của ARIA là đừng dùng ARIA, hãy dùng một thẻ HTML gốc có sẵn ngữ nghĩa đúng. Bài học hôm nay sẽ giải thích tại sao lời khuyên đó lại đúng đến vậy, và làm thế nào để bạn viết HTML "có nghĩa" ngay từ đầu.
Điều tuyệt vời nhất: semantic HTML gần như miễn phí. Nó không đòi hỏi thư viện, không tốn thêm dòng JavaScript nào, không làm chậm trang. Bạn chỉ cần chọn đúng thẻ cho đúng việc. Đây là kỹ năng có tỷ lệ lợi ích trên chi phí cao nhất trong toàn bộ khóa học.
Khái niệm cốt lõi
"Ngữ nghĩa" nghĩa là gì?
Mỗi thẻ HTML mang theo một ý nghĩa (semantics) — nó nói cho trình duyệt, trình đọc màn hình, công cụ tìm kiếm và các công nghệ hỗ trợ (assistive technology) biết "đây là cái gì". Thẻ <button> nói rằng "đây là một nút bấm có thể kích hoạt". Thẻ <nav> nói "đây là khu vực điều hướng". Thẻ <h1> nói "đây là tiêu đề quan trọng nhất trang".
Ngược lại, <div> và <span> là những thẻ vô nghĩa (non-semantic). Chúng chỉ là những chiếc hộp trống rỗng để gom nhóm và tạo kiểu. Với con mắt người sáng, một <div> được style đẹp trông y hệt một nút bấm. Nhưng với trình đọc màn hình, <div> chỉ là... không gì cả. Nó không được đọc lên như một nút, không nằm trong danh sách các phần tử tương tác được, không phản hồi phím Enter hay Space.
Cây accessibility (Accessibility Tree)
Đây là khái niệm mấu chốt cần nắm. Khi trình duyệt dựng trang, nó tạo ra DOM tree (cây tài liệu để hiển thị) và một cây song song gọi là accessibility tree. Chính cây này mới là thứ trình đọc màn hình đọc. Cây accessibility rút gọn DOM thành những thông tin mà công nghệ hỗ trợ cần: mỗi phần tử có role (vai trò — là nút? là link? là heading?), name (tên đọc lên), state (trạng thái — đang mở? đã chọn?).
Khi bạn viết <button>Gửi</button>, trình duyệt tự động điền vào cây accessibility: role = "button", name = "Gửi", và gắn sẵn hành vi bàn phím. Khi bạn viết <div onclick="submit()">Gửi</div>, cây accessibility chỉ thấy một khối văn bản "Gửi" không có role, không có ý nghĩa tương tác. Người dùng screen reader lướt qua sẽ không hề biết đó là thứ bấm được.
<button> ≠ <div onclick> — ví dụ kinh điển
Hãy phân tích kỹ sự khác biệt này vì nó là bài học lớn nhất của bài. Khi bạn dùng đúng <button>, bạn nhận được miễn phí một loạt hành vi:
- Có thể focus bằng bàn phím (tab tới được) — mặc định
<div>không có trong tab order. - Kích hoạt bằng Enter và Space —
<div>chỉ phản hồi click chuột. - Được thông báo là "button" cho screen reader — người khiếm thị biết đây là nút.
- Nằm đúng trong danh sách các phần tử tương tác khi người dùng liệt kê nhanh.
- Hỗ trợ trạng thái
disabledđúng cách.
<div>, bạn phải viết tay: tabindex="0", role="button", aria-pressed nếu cần, thêm event listener cho cả keydown (bắt Enter và Space, còn phải chặn cuộn trang khi bấm Space), xử lý disabled thủ công... Hàng chục dòng code để tái tạo thứ mà một thẻ <button> cho không. Đây chính là lý do quy tắc "đừng dùng ARIA nếu có thẻ gốc" tồn tại.Các nhóm thẻ semantic quan trọng
Thẻ tương tác: <button>, <a href>, <input>, <select>, <textarea>, <label>. Đây là những thẻ mang hành vi. Bài 20 sẽ đào sâu button vs link, nhưng nguyên tắc chung: nút để thực hiện hành động, link để điều hướng đến địa chỉ.
Thẻ cấu trúc trang (landmarks): <header>, <nav>, <main>, <aside>, <footer>, <section>, <article>. Chúng tạo ra các "vùng đất mốc" giúp người dùng screen reader nhảy nhanh giữa các khu vực. (Bài 18 sẽ nói kỹ về landmarks.)
Thẻ nội dung có nghĩa: <h1>–<h6> cho tiêu đề, <ul>/<ol>/<li> cho danh sách, <p> cho đoạn văn, <table> cho dữ liệu bảng, <figure>/<figcaption> cho hình minh họa, <time>, <address>. Một danh sách viết bằng <ul><li> sẽ được screen reader đọc là "danh sách, 5 mục" — cực kỳ hữu ích. Cùng nội dung đó viết bằng các <div> xuống dòng thì chỉ là một mớ chữ rời rạc.
Tình huống thực tế
Ví dụ 1 — Sàn thương mại điện tử và nút "Thêm vào giỏ" tàng hình
Một đội phát triển tại một startup thương mại điện tử ở TP.HCM (tạm gọi ShopNhanh) xây trang sản phẩm bằng React. Để "kiểm soát style hoàn toàn", họ dùng <div className="btn-add" onClick={addToCart}>Thêm vào giỏ</div> cho tất cả các nút. Nhìn bằng mắt thì hoàn hảo.
Vấn đề phát sinh khi một khách hàng khiếm thị dùng NVDA (một screen reader phổ biến) phản hồi qua email rằng anh không thể mua hàng: trình đọc lướt qua trang, liệt kê được 12 button, nhưng nút "Thêm vào giỏ" quan trọng nhất lại không nằm trong danh sách đó — vì nó là <div>. Anh không thể tab tới, không bấm Enter được. Tỷ lệ chuyển đổi từ nhóm người dùng trợ năng gần như bằng 0.
Cả đội mất hai ngày rà soát và thay thế <div onClick> bằng <button> trên toàn hệ thống, gỡ bỏ cả đống CSS reset thủ công. Kết quả: nút hoạt động ngay với bàn phím và screen reader, code JavaScript ngắn hơn trước, và họ không cần dòng tabindex hay role nào.
Bài học: Style và ngữ nghĩa là hai chuyện tách biệt. Bạn có thể <button> trông giống bất cứ thứ gì bằng CSS (background: none; border: none; padding: 0). Đừng bao giờ hy sinh ngữ nghĩa để đổi lấy sự tiện lợi khi style.
Ví dụ 2 — Cổng dịch vụ công và "menu" bằng div
Một nhà thầu xây dựng cổng thông tin cho một sở tại Đà Nẵng. Thanh điều hướng chính được dựng bằng <div class="menu"><div class="menu-item">...</div></div>, các liên kết là <span onclick="location.href=...">. Trong đợt kiểm định trước khi nghiệm thu, một chuyên gia audit chạy công cụ và test thủ công bằng phím Tab: toàn bộ menu không tab tới được. Người dùng chỉ dùng bàn phím — bao gồm cả người khuyết tật vận động — hoàn toàn không điều hướng được website của cơ quan nhà nước.
Đơn vị này còn chịu ràng buộc pháp lý về khả năng tiếp cận (bài 46 sẽ bàn về khía cạnh pháp lý). Giải pháp: thay khối menu bằng <nav><ul><li><a href="/thu-tuc">Thủ tục</a></li>...</ul></nav>. Ngay lập tức Tab hoạt động, screen reader thông báo "navigation, list, 6 items", và người dùng có thể nhảy thẳng đến vùng navigation bằng phím tắt landmark.
Bài học: Điều hướng phải dùng thẻ link thật (<a href>) đặt trong <nav>. <span onclick> giả làm link không có href sẽ không xuất hiện trong danh sách link, không mở được ở tab mới, và vô hình với bàn phím.
Ví dụ 3 — App ngân hàng và bảng sao kê bằng div
Một fintech ở Đông Nam Á dựng bảng lịch sử giao dịch bằng các <div> xếp thành hàng cột bằng CSS Grid — trông rất "modern". Nhưng khi người dùng screen reader nghe qua, họ chỉ thấy một chuỗi số và chữ trôi liên tục: "15/06 500000 Chuyển tiền 12/06 200000 Rút tiền..." không rõ con số nào ứng với cột nào, không biết đâu là ngày, đâu là số dư.
Với <table> đúng chuẩn (<thead>, <th scope="col">, <tbody>, <td>), screen reader đọc: "Ngày: 15/06, Số tiền: 500.000, Loại: Chuyển tiền" — mỗi ô được đọc kèm tên cột tương ứng. (Bài 19 sẽ chuyên sâu về bảng.) Đội fintech chuyển sang <table> cho dữ liệu dạng bảng và giữ Grid CSS chỉ cho layout trang.
Bài học: Dữ liệu có quan hệ hàng-cột là dữ liệu bảng — hãy dùng <table>. Đừng dùng <table> để làm layout, và cũng đừng dùng <div> grid để trình bày dữ liệu bảng. Chọn thẻ theo bản chất nội dung, không theo hình thức hiển thị.
Hướng dẫn từng bước
Bước 1 — Hỏi "đây là cái gì?" trước khi hỏi "trông thế nào?" Trước mỗi khối giao diện, tự hỏi bản chất ngữ nghĩa: Đây là hành động (button)? Đường dẫn (link)? Tiêu đề (heading)? Danh sách? Vùng cấu trúc? Chọn thẻ theo câu trả lời đó, rồi mới style.
Bước 2 — Ưu tiên thẻ HTML gốc.
Nếu HTML đã có sẵn thẻ đúng ngữ nghĩa, dùng nó. Nút bấm → <button>. Liên kết điều hướng → <a href>. Ô nhập → <input>. Đừng tái phát minh bằng <div> + JavaScript + ARIA.
Bước 3 — Dựng khung trang bằng landmarks.
Bọc nội dung trong <header>, <nav>, <main> (chỉ một <main> mỗi trang), <footer>. Điều này cho người dùng screen reader "bản đồ" để nhảy quanh trang.
Bước 4 — Dùng đúng thẻ nội dung.
Tiêu đề dùng <h1>–<h6> theo thứ tự phân cấp, không nhảy cóc. Danh sách dùng <ul>/<ol>. Đoạn văn dùng <p>. Bảng dữ liệu dùng <table>.
Bước 5 — Kiểm tra nhanh bằng bàn phím. Nhấn phím Tab lướt qua trang. Mọi thứ tương tác được bằng chuột cũng phải tab tới được và kích hoạt bằng Enter/Space. Nếu một phần tử bấm được mà Tab bỏ qua, gần như chắc chắn bạn đã dùng sai thẻ.
Bước 6 — Soi cây accessibility.
Mở DevTools của trình duyệt (tab Accessibility trong Chrome/Firefox), xem role và name của từng phần tử. Nếu một nút hiển thị role "generic" thay vì "button", đó là dấu hiệu bạn dùng <div> thay vì <button>.
Lỗi thường gặp & mẹo
Lỗi 1 — "Div-itis": lạm dụng <div> cho mọi thứ. Đây là bệnh phổ biến nhất, thường do quen với các UI library hoặc copy code không cẩn thận. Mẹo: coi mỗi <div onClick> như một "cờ đỏ" cần soi lại — 90% trường hợp nên là <button>.
Lỗi 2 — Link không có href. <a onClick> không có thuộc tính href sẽ không focus được bằng bàn phím và không được đọc là link. Nếu là điều hướng, phải có href. Nếu là hành động không đi đâu, hãy dùng <button>.
Lỗi 3 — Nhầm giữa nút và link. Quy tắc vàng: dùng <a> khi bấm sẽ đi đến một URL (có thể copy, mở tab mới, bookmark). Dùng <button> khi bấm thực hiện hành động ngay trên trang (mở modal, gửi form, xóa mục). Bài 20 sẽ đào sâu, nhưng nhớ nguyên tắc này ngay từ giờ.
Lỗi 4 — Dùng heading để phóng to chữ. Nhiều người chọn <h3> chỉ vì "chữ to vừa đẹp". Heading là cấu trúc, không phải kích cỡ. Muốn chữ to thì dùng CSS font-size. Đặt sai heading làm rối "mục lục" mà screen reader dựng ra.
Lỗi 5 — Danh sách giả. Viết các mục bằng <br> hoặc nhiều <div> thay vì <ul><li>. Screen reader mất thông tin "đây là danh sách có N mục".
Mẹo vàng: Khi bạn thấy mình sắp thêm tabindex="0" và role="button" vào một <div>, hãy dừng lại và tự hỏi: "Tại sao mình không dùng luôn <button>?" Trong hầu hết trường hợp, câu trả lời là "chẳng có lý do gì cả" — và bạn vừa tiết kiệm được cả đống code lẫn bug tiềm ẩn.
Bài tập thực hành
- Audit một trang có sẵn: Mở một website bạn hay dùng, nhấn Tab liên tục và quan sát. Có phần tử nào bấm được bằng chuột mà Tab bỏ qua không? Ghi lại ít nhất 3 chỗ và đoán xem chúng đang dùng thẻ gì sai.
- Refactor một khối
<div>: Cho đoạn code sau, hãy viết lại bằng semantic HTML đúng:
<div class="card">
<div class="title">Khóa học Accessibility</div>
<div class="desc">Học thiết kế bao trùm.</div>
<div class="link" onclick="location.href='/course/7'">Xem chi tiết</div>
<div class="btn" onclick="enroll()">Đăng ký ngay</div>
</div>
Gợi ý: title là heading, "Xem chi tiết" điều hướng đến URL (link), "Đăng ký ngay" thực hiện hành động (button).- Dựng khung landmark: Viết bộ khung HTML cho một trang blog gồm: header có logo và menu điều hướng, vùng nội dung chính với 3 bài viết, thanh bên hiển thị bài liên quan, và footer. Chỉ dùng thẻ semantic, không dùng
<div>cho cấu trúc chính.
- Kiểm chứng bằng DevTools: Với bài tập 2, mở tab Accessibility trong trình duyệt và xác nhận nút "Đăng ký ngay" hiển thị role "button", còn "Xem chi tiết" hiển thị role "link".
Tóm tắt
Semantic HTML là nền tảng gốc rễ của accessibility — mọi kỹ thuật khác đều xây trên nó. Ý tưởng cốt lõi vô cùng đơn giản: chọn đúng thẻ cho đúng việc. Mỗi thẻ HTML mang một ý nghĩa mà trình duyệt tự động dịch thành role, name, state trong cây accessibility — thứ mà trình đọc màn hình thực sự đọc.
Sự khác biệt giữa <button> và <div onclick> gói gọn cả bài học: thẻ gốc cho bạn miễn phí khả năng focus bàn phím, kích hoạt bằng Enter/Space, và thông báo đúng role cho công nghệ hỗ trợ — những thứ mà <div> buộc bạn phải tái tạo tốn kém và dễ sai. Vì vậy quy tắc số một là ưu tiên thẻ HTML gốc, chỉ tự chế bằng ARIA khi thật sự không còn lựa chọn.
Hãy nhớ ba điều: (1) hỏi "đây là cái gì?" trước khi style; (2) dùng landmarks để dựng khung trang, thẻ nội dung đúng cho danh sách/heading/bảng; (3) test bằng phím Tab — nếu bấm được bằng chuột thì cũng phải dùng được bằng bàn phím. Nắm chắc nền tảng này, bạn đã đi được nửa chặng đường tới một sản phẩm bao trùm. Ở các bài tiếp theo, chúng ta sẽ xây thêm ARIA, quản lý focus và các component phức tạp — tất cả trên nền semantic HTML vững chắc mà bạn vừa học.