Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 8 — ARIA Basics — Roles, States, Properties

Đã đọc 0/13 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn vừa hoàn thành một giao diện quản trị đẹp mắt cho một startup fintech ở TP.HCM. Bạn dùng <div> để dựng nút bấm, <span> để làm menu thả xuống, và một loạt JavaScript tinh tế để mọi thứ chạy mượt mà. Trên màn hình, tất cả trông hoàn hảo. Nhưng khi một người dùng khiếm thị bật trình đọc màn hình (screen reader) lên, họ chỉ nghe thấy... sự im lặng. Trình đọc màn hình đọc "div, div, div" một cách vô hồn, không biết cái nào là nút bấm, cái nào đang mở, cái nào có thể click được. Toàn bộ giao diện trở nên vô hình với họ.

Đây chính là khoảng trống mà ARIA sinh ra để lấp đầy. Ở những bài trước, bạn đã học rằng Semantic HTML (Bài 7) là nền tảng vững chắc nhất cho accessibility — dùng <button> thật thay vì <div> giả. Nhưng thế giới web hiện đại đầy rẫy những component phức tạp mà HTML thuần túy không có sẵn: tab, accordion, combobox, modal, thanh trượt tùy chỉnh. Khi HTML "hết chữ" để mô tả những thứ này, ARIA bước vào như một lớp phiên dịch, giúp công nghệ hỗ trợ hiểu được những gì đang diễn ra.

Bài này sẽ giúp bạn nắm vững bộ ba nền tảng của ARIA: Roles (vai trò), States (trạng thái) và Properties (thuộc tính). Đây là ngữ pháp cốt lõi mà bạn sẽ dùng đi dùng lại trong suốt phần còn lại của khóa học. Quan trọng hơn, bạn sẽ học được khi nào nên dùng và đặc biệt là khi nào KHÔNG nên dùng ARIA — vì ARIA dùng sai còn tệ hơn không dùng gì cả.

Khái niệm cốt lõi

ARIA là gì?

ARIA là viết tắt của Accessible Rich Internet Applications — một bộ quy chuẩn (specification) do W3C ban hành. Mục đích của nó rất cụ thể: bổ sung thông tin về accessibility cho các giao diện động (dynamic UI) khi bản thân HTML không đủ khả năng diễn đạt.

Điểm mấu chốt bạn cần khắc cốt ghi tâm: ARIA không thay đổi bất cứ hành vi nào của trình duyệt. Nó không làm cho một <div> có thể focus bằng phím Tab, không tự thêm sự kiện click, không xử lý phím bàn phím. ARIA chỉ làm đúng một việc — thay đổi cái mà trình đọc màn hình thông báo cho người dùng. Nói cách khác, ARIA sửa "lời nói" chứ không sửa "hành động". Bạn vẫn phải tự viết JavaScript để xử lý hành vi.

ARIA hoạt động thông qua Accessibility Tree — một cấu trúc song song với DOM mà trình duyệt tạo ra để cung cấp cho công nghệ hỗ trợ. Khi bạn thêm một thuộc tính ARIA, bạn đang chỉnh sửa cây accessibility này, không phải giao diện hình ảnh.

Ba trụ cột: Roles, States, Properties

Roles (Vai trò) — trả lời câu hỏi "đây là cái gì?". Role định nghĩa loại của một phần tử: nó là nút bấm, thanh điều hướng, hộp thoại hay thanh tiến trình? Ví dụ role="button", role="navigation", role="dialog", role="alert". Role thường không thay đổi trong vòng đời của phần tử.

<div role="button" tabindex="0">Gửi đơn hàng</div>

States (Trạng thái) — trả lời câu hỏi "hiện tại nó đang thế nào?". Trạng thái có thể thay đổi liên tục do tương tác của người dùng. Ví dụ aria-expanded (đang mở hay đóng), aria-checked (đã tick chưa), aria-disabled (đang bị vô hiệu hóa), aria-hidden (có bị ẩn khỏi trình đọc không). Khi trạng thái đổi, bạn phải cập nhật giá trị này bằng JavaScript.

<button aria-expanded="false">Xem chi tiết</button>
<!-- Khi người dùng bấm mở, JS đổi thành aria-expanded="true" -->

Properties (Thuộc tính) — trả lời câu hỏi "nó có đặc điểm gì?". Đây là những đặc tính thường ổn định hơn state, mô tả mối quan hệ hoặc bản chất của phần tử. Ví dụ aria-label (nhãn văn bản), aria-labelledby (nhãn lấy từ phần tử khác), aria-describedby (mô tả bổ sung), aria-required (bắt buộc nhập), aria-controls (điều khiển phần tử nào).

<button aria-label="Đóng cửa sổ" aria-controls="popup-menu">✕</button>

Ranh giới giữa State và Property đôi khi hơi mờ, và thật lòng mà nói, bạn không cần phải phân biệt hoàn hảo trong thực tế. Điều quan trọng là hiểu: role nói "là gì", còn state/property bổ sung các chi tiết về trạng thái và quan hệ.

Năm quy tắc vàng của ARIA (The Rules of ARIA)

W3C đã đúc kết ra năm nguyên tắc sử dụng ARIA. Đây là kim chỉ nam bạn nên thuộc lòng:

Quy tắc 1 — Đừng dùng ARIA nếu có thể dùng HTML thật. Nếu bạn cần một nút, hãy dùng <button>. Nếu cần ô đánh dấu, dùng <input type="checkbox">. HTML gốc đã có sẵn role, state, hành vi bàn phím và focus — miễn phí. Chỉ khi HTML không có sẵn thứ bạn cần thì mới nghĩ tới ARIA. Có một câu nói nổi tiếng trong cộng đồng a11y: "No ARIA is better than bad ARIA" — không dùng ARIA còn tốt hơn dùng ARIA sai.

Quy tắc 2 — Đừng thay đổi semantic gốc trừ khi thực sự cần. Đừng viết <h2 role="button"> vì làm vậy trình đọc màn hình sẽ không còn coi nó là tiêu đề nữa. Nếu cần một tiêu đề mà bấm được, hãy bọc một <button> bên trong <h2>.

Quy tắc 3 — Mọi control ARIA tương tác phải dùng được bằng bàn phím. Nếu bạn tạo role="button" trên một div, bạn phải tự thêm tabindex="0" và xử lý cả phím Enter lẫn Space. Đây là lý do vì sao dùng <button> gốc luôn nhẹ nhàng hơn.

Quy tắc 4 — Đừng dùng role="presentation" hay aria-hidden="true" lên phần tử có thể focus. Nếu bạn ẩn một nút khỏi trình đọc màn hình nhưng người dùng vẫn Tab tới được nó, họ sẽ rơi vào một "hố đen" — focus ở đâu đó nhưng screen reader không đọc gì.

Quy tắc 5 — Mọi phần tử tương tác phải có tên (accessible name). Một nút chỉ có icon ✕ mà không có aria-label sẽ được đọc là "button" trống rỗng. Luôn đảm bảo control có nhãn rõ ràng.

Tình huống thực tế

Ví dụ 1: Nút "giả" bằng div tại một sàn TMĐT Việt Nam

Một sàn thương mại điện tử giả định — gọi là ChợNhanh — thuê một agency dựng lại trang chi tiết sản phẩm. Vì lý do styling, đội frontend dùng <div class="btn-add-cart">Thêm vào giỏ</div> thay cho <button>. Trên chuột, mọi thứ hoạt động vì họ gắn onclick.

Ba tuần sau ra mắt, bộ phận CSKH nhận phản hồi từ một khách hàng khiếm thị: anh không thể mua hàng. Khi kiểm tra bằng trình đọc màn hình NVDA, đội ngũ phát hiện nút "Thêm vào giỏ" hoàn toàn không tồn tại với screen reader — nó không đọc, không Tab tới được, không bấm bằng phím được.

Cách chữa cháy nhanh của họ là biến div thành control ARIA đầy đủ:

<div role="button" tabindex="0" aria-label="Thêm vào giỏ hàng">
  Thêm vào giỏ
</div>

Kèm JavaScript xử lý phím Enter và Space. Nó chạy được, nhưng đội ngũ nhận ra họ đã tự làm khó mình: phải viết thêm 15 dòng code chỉ để tái tạo những gì <button> cho không. Bài học: ARIA có thể vá lỗi, nhưng lựa chọn đúng ngay từ đầu (dùng <button>) rẻ hơn nhiều. Đây là minh họa sống động cho Quy tắc 1.

Ví dụ 2: aria-expanded cho menu tài khoản ở một ngân hàng số

Một ngân hàng số giả định tại Singapore triển khai menu thả xuống ở góc phải cho phần "Tài khoản của tôi". Ban đầu, menu chỉ hiện/ẩn bằng CSS khi bấm. Người dùng sáng mắt thấy rõ menu bung ra, nhưng người dùng screen reader thì mù tịt — họ bấm vào nút và không biết có gì vừa xảy ra, menu đã mở hay chưa.

Đội a11y bổ sung aria-expanded và aria-controls:

<button aria-expanded="false" aria-controls="account-menu" id="account-btn">
  Tài khoản của tôi
</button>
<ul id="account-menu" hidden>
  <li><a href="/profile">Hồ sơ</a></li>
  <li><a href="/logout">Đăng xuất</a></li>
</ul>

Mỗi khi người dùng bấm mở, JavaScript đổi aria-expanded thành "true" và bỏ thuộc tính hidden. Giờ đây trình đọc màn hình thông báo: "Tài khoản của tôi, nút, thu gọn" rồi sau khi bấm là "mở rộng". Người dùng biết chính xác chuyện gì đang diễn ra. Bài học: state như aria-expanded phải được JavaScript cập nhật đồng bộ với thay đổi hình ảnh — nếu quên cập nhật, ARIA nói dối và còn tệ hơn không có.

Ví dụ 3: aria-label lạm dụng làm hỏng nút tại một app giao đồ ăn

Một ứng dụng giao đồ ăn ở Hà Nội có nút "Đặt lại đơn" kèm icon. Lập trình viên, vì cẩn thận thái quá, viết:

<button aria-label="reorder">
  <i class="icon-refresh"></i> Đặt lại đơn
</button>

Vấn đề: nút đã có sẵn văn bản "Đặt lại đơn" nhìn thấy được, nhưng aria-label="reorder" (tiếng Anh, và ghi đè hoàn toàn) khiến trình đọc màn hình đọc là "reorder" thay vì "Đặt lại đơn". Người dùng Việt Nam nghe một từ tiếng Anh vô nghĩa. Tệ hơn, nó phá vỡ tính năng điều khiển bằng giọng nói: người dùng nói "bấm Đặt lại đơn" nhưng hệ thống tìm nút tên "reorder" nên không khớp.

Cách sửa: xóa aria-label đi hoàn toàn. Văn bản hiển thị đã là accessible name tốt nhất rồi. Bài học: aria-label ghi đè nội dung hiển thị — chỉ dùng khi nút không có văn bản (ví dụ nút chỉ có icon), và nếu dùng thì phải khớp ngôn ngữ, khớp với chữ mà mắt thường thấy.

Hướng dẫn từng bước

Khi đứng trước một component và tự hỏi "có cần ARIA không?", hãy đi theo quy trình sau:

Bước 1 — Hỏi: HTML gốc có làm được không? Trước khi gõ một ký tự ARIA nào, kiểm tra xem có phần tử HTML nào sẵn có phù hợp không. Cần nút? <button>. Cần checkbox? <input type="checkbox">. Cần điều hướng? <nav>. Nếu có, dừng tại đây — bạn không cần ARIA.

Bước 2 — Nếu buộc phải dùng div, gán role phù hợp. Khi không tránh được (ví dụ component tùy chỉnh không có tương đương HTML), xác định role đúng bằng cách tra bảng WAI-ARIA Authoring Practices. Ví dụ với một tab tùy chỉnh: role="tablist", role="tab", role="tabpanel".

Bước 3 — Gán accessible name. Đảm bảo mỗi control tương tác có tên: hoặc bằng văn bản bên trong, hoặc aria-label, hoặc aria-labelledby. Ưu tiên aria-labelledby khi nhãn đã hiển thị đâu đó trên trang.

Bước 4 — Thêm state ban đầu và kết nối với JavaScript. Khai báo giá trị ban đầu (ví dụ aria-expanded="false"), rồi viết JS để cập nhật mỗi khi trạng thái đổi. Đây là bước hay bị quên nhất.

Bước 5 — Bổ sung hành vi bàn phím. Thêm tabindex="0" để focus được, và xử lý các phím tương ứng (Enter/Space cho button, mũi tên cho tab...).

Bước 6 — Kiểm thử thật. Bật một trình đọc màn hình (NVDA trên Windows miễn phí, VoiceOver có sẵn trên macOS) và điều hướng chỉ bằng bàn phím. Nghe xem nó đọc có đúng ý bạn không. Đây là bước không thể bỏ qua — chúng ta sẽ đi sâu hơn ở các bài về testing sau này.

Lỗi thường gặp & mẹo

Lỗi: Thêm role trùng với semantic gốc. Viết <button role="button"> là thừa và vô nghĩa. <nav role="navigation"> cũng vậy. HTML5 đã có sẵn role ngầm định (implicit role). Đừng lặp lại.

Lỗi: Đặt aria-expanded lên sai phần tử. aria-expanded phải nằm trên phần tử điều khiển việc mở/đóng (thường là nút bấm), không phải trên phần tử nội dung bị mở ra.

Lỗi: Quên cập nhật state bằng JS. Khai báo aria-checked="false" rồi để nguyên mãi mãi là lỗi kinh điển. State tĩnh là state dối trá. Nếu giá trị không bao giờ đổi, có lẽ bạn đang dùng sai thuộc tính.

Lỗi: Lạm dụng aria-hidden="true". Nhiều người ẩn cả một vùng lớn khỏi screen reader mà không nhận ra bên trong có nút Tab tới được. Nhớ Quy tắc 4: đừng ẩn thứ có thể focus.

Mẹo — role="alert" cho thông báo động. Khi bạn muốn screen reader đọc ngay một thông báo (ví dụ "Đã thêm vào giỏ"), đặt nó trong một vùng role="alert". Trình đọc sẽ tự thông báo mà không cần người dùng di chuyển focus tới.

Mẹo — Ưu tiên aria-labelledby hơn aria-label khi có thể. aria-labelledby trỏ tới văn bản đã hiển thị, nên tự động đồng bộ ngôn ngữ và nội dung, tránh được lỗi như Ví dụ 3.

Mẹo — Kiểm tra bằng tab Accessibility trong DevTools. Chrome/Edge có tab "Accessibility" hiển thị computed role và accessible name của mỗi phần tử. Đây là cách nhanh để xác nhận ARIA của bạn có ăn không.

Bài tập thực hành

  • Chuyển đổi nút giả: Tìm (hoặc tự tạo) một <div> đóng vai nút bấm. Biến nó thành control ARIA đầy đủ với role="button", tabindex="0", aria-label phù hợp, và JavaScript xử lý phím Enter/Space. Sau đó, viết lại bằng <button> gốc và so sánh số dòng code — cảm nhận sự khác biệt.
  • Xây menu thả xuống có aria-expanded: Tạo một nút "Menu" điều khiển một danh sách. Dùng aria-expanded và aria-controls, đảm bảo JavaScript cập nhật state mỗi khi mở/đóng. Bật VoiceOver hoặc NVDA và nghe nó thông báo "mở rộng"/"thu gọn".
  • Săn lỗi aria-label: Rà soát một trang bất kỳ bạn từng làm. Tìm các nút có aria-label mà đã có sẵn văn bản hiển thị, hoặc aria-label sai ngôn ngữ. Sửa lại cho đúng nguyên tắc "nhãn khớp với chữ mắt thấy".
  • Thử nghiệm role="alert": Tạo một nút "Lưu", khi bấm sẽ chèn dòng chữ "Đã lưu thành công" vào một vùng role="alert". Xác nhận trình đọc màn hình tự đọc mà không cần bạn di chuyển focus.

Tóm tắt

ARIA (Accessible Rich Internet Applications) là bộ quy chuẩn của W3C giúp bổ sung thông tin accessibility cho giao diện động khi HTML thuần không đủ. Nó gồm ba trụ cột: Roles (đây là gì), States (đang thế nào — có thể thay đổi), và Properties (có đặc điểm/quan hệ gì).

Điều quan trọng nhất cần nhớ: ARIA chỉ thay đổi thông tin mà screen reader thông báo, không thay đổi hành vi trình duyệt. Bạn vẫn phải tự lo focus và bàn phím. Vì vậy, hãy luôn ưu tiên HTML gốc trước — "No ARIA is better than bad ARIA". Chỉ dùng ARIA khi thực sự cần, gán accessible name cho mọi control, và đừng bao giờ quên cập nhật state bằng JavaScript để ARIA luôn nói thật.

Ở các bài tiếp theo, bạn sẽ áp dụng chính bộ ngữ pháp này để dựng những component phức tạp như modal, tab, accordion và combobox một cách accessible. Nền tảng Roles-States-Properties bạn vừa học chính là bộ công cụ bạn sẽ dùng đi dùng lại suốt phần còn lại của khóa học.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi