Product Management
Đăng nhập
ESC

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

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

Bài 27 — Tooltip A11y

Đã đọc 0/15 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

Tooltip là một trong những component "nhỏ mà có võ" nhất trong thiết kế giao diện. Nó chỉ là một mẩu chú thích nhỏ bật lên khi bạn di chuột vào một biểu tượng, một cái nút, hay một từ khó hiểu. Nhìn thì đơn giản, nhưng trên thực tế đây lại là một trong những component bị làm sai nhiều nhất về mặt accessibility. Lý do rất dễ hiểu: tooltip mặc định được thiết kế cho chuột — bạn phải "hover" (rê chuột) mới thấy. Nhưng người dùng bàn phím thì không có chuột. Người dùng screen reader thì không nhìn thấy con trỏ. Người dùng trên điện thoại thì chẳng có khái niệm "hover" nào cả.

Hãy hình dung một tình huống rất Việt Nam: một ngân hàng số làm nút "Chuyển tiền nhanh 24/7" chỉ bằng một biểu tượng mũi tên, và giải thích chức năng bằng tooltip khi hover. Một khách hàng lớn tuổi dùng phím Tab để duyệt trang, hoặc một người khiếm thị dùng screen reader — họ sẽ chạm tới cái nút đó nhưng hoàn toàn không biết nó làm gì, vì tooltip không bao giờ xuất hiện cho họ. Kết quả: họ không dám bấm, hoặc bấm nhầm. Đây không phải giả thuyết — đó là kiểu lỗi khiến một tính năng tưởng chừng đã "hoàn thiện" lại loại trừ hàng nghìn người.

Bài này sẽ dạy bạn cách làm tooltip đúng chuẩn accessibility: khi nào nên dùng, khi nào KHÔNG nên dùng, dùng aria-describedby hay aria-labelledby, và làm sao để tooltip hoạt động được với cả chuột, bàn phím lẫn screen reader. Đây là phần rất riêng của Bài 27 — chúng ta chỉ nói về tooltip, không lấn sang tooltip-vs-toggletip nói chung của các bài dropdown hay dialog.

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

Tooltip là gì và không là gì

Trong ngữ cảnh accessibility, tooltip là một mẩu văn bản mô tả ngắn, không tương tác được, xuất hiện khi người dùng focus hoặc hover vào một phần tử điều khiển (thường là nút hoặc link). Từ khóa quan trọng: không tương tác được. Tooltip chỉ chứa text thuần — không nút, không link, không form bên trong. Nếu bạn cần bỏ một cái link hay nút vào trong đó, thì cái bạn đang làm không còn là tooltip nữa, mà là một popover hoặc toggletip — và chúng có pattern accessibility khác hẳn.

Tooltip cũng luôn gắn với một phần tử điều khiển có sẵn. Nó bổ sung thông tin, chứ không thay thế. Đây là điểm phân biệt cực kỳ quan trọng dẫn tới hai vai trò khác nhau của ARIA mà chúng ta sẽ nói ngay dưới đây.

aria-describedby vs aria-labelledby

Đây là quyết định thiết kế quan trọng nhất khi làm tooltip, và rất nhiều người làm sai. Câu hỏi bạn phải tự trả lời là: Phần tử điều khiển đã có tên (accessible name) chưa?

  • Nếu nút đã có text nhìn thấy được (ví dụ nút hiển thị chữ "Settings"), thì tooltip đóng vai trò mô tả bổ sung → dùng aria-describedby. Screen reader sẽ đọc "Settings, button" trước, rồi đọc nội dung tooltip như phần mô tả thêm.
  • Nếu nút chỉ có biểu tượng, không có text nào cả (ví dụ nút chỉ có icon hình bánh răng), thì bản thân tooltip chính là tên của nút → dùng aria-labelledby. Lúc này screen reader sẽ đọc nội dung tooltip như tên của nút: "Settings, button".
Nhìn theo dàn ý gốc của bài:

<button aria-describedby="tooltip-1">
  <svg>...</svg> Settings
</button>
<div role="tooltip" id="tooltip-1" hidden>
  Điều chỉnh cài đặt tài khoản của bạn
</div>

Ở đây nút đã có chữ "Settings" nhìn thấy được, nên aria-describedby là đúng: tooltip bổ sung thông tin "Điều chỉnh cài đặt tài khoản của bạn". Nhưng nếu nút chỉ có icon:

<button aria-labelledby="tooltip-2">
  <svg aria-hidden="true">...</svg>
</button>
<div role="tooltip" id="tooltip-2" hidden>Cài đặt</div>

Lúc này tooltip "Cài đặt" chính là tên của nút. Lưu ý aria-hidden="true" trên <svg> để screen reader không cố đọc icon.

role="tooltip" và cách ẩn/hiện đúng

Phần tử chứa nội dung tooltip nên có role="tooltip". Vai trò này báo cho công nghệ hỗ trợ biết đây là một chú thích. Nhưng lưu ý: bản thân role="tooltip" không tự động làm cho screen reader đọc nó — chính mối liên kết aria-describedby/aria-labelledby từ phần tử điều khiển mới là thứ khiến nó được đọc.

Về ẩn/hiện: dùng thuộc tính hidden hoặc display: none khi tooltip đóng. Điều then chốt là không dùng những kỹ thuật ẩn về mặt hình ảnh mà vẫn để lộ với screen reader (như opacity: 0 hay đẩy ra ngoài màn hình) cho trạng thái đóng — nếu không, screen reader sẽ đọc tooltip liên tục kể cả khi nó đang "ẩn". Khi tooltip mở, bạn bỏ hidden đi.

Ba điều kiện WCAG bắt buộc: hoverable, dismissible, persistent

WCAG 2.1 có một tiêu chí tên là 1.4.13 Content on Hover or Focus áp dụng trực tiếp cho tooltip. Nội dung xuất hiện khi hover/focus phải thỏa mãn ba điều:

  • Dismissible (đóng được): người dùng phải đóng được tooltip mà không cần di chuột đi hay bỏ focus — thường là bằng phím Esc. Điều này quan trọng cho người dùng phóng to màn hình, vì tooltip có thể che mất nội dung họ đang cần đọc.
  • Hoverable (rê chuột lên được): nếu người dùng di chuột từ nút sang chính cái tooltip, tooltip không được biến mất. Đây là lý do nhiều tooltip "nhấp nháy" khi bạn cố đọc — vì có khoảng trống giữa nút và tooltip khiến mouseleave kích hoạt.
  • Persistent (giữ lại): tooltip phải giữ hiển thị cho tới khi người dùng chủ động bỏ hover, bỏ focus, hoặc bấm Esc. Không được tự tắt sau vài giây.

Trigger phải bằng cả focus lẫn hover

Nguyên tắc vàng: nếu tooltip hiện khi hover, thì nó BẮT BUỘC cũng phải hiện khi focus (khi người dùng Tab tới). Điều này đảm bảo người dùng bàn phím tiếp cận được thông tin y hệt người dùng chuột. Vì vậy phần tử điều khiển phải focus được — nghĩa là dùng <button> hoặc <a> thật, chứ không phải một cái <div> hay <span> trơn.

Tình huống thực tế

Tình huống 1 — Ngân hàng số Việt Nam và cái nút icon "im lặng"

Một đội sản phẩm tại một ngân hàng số ở TP.HCM (giả định, tương tự mô hình các ngân hàng số đang phát triển tại Việt Nam) thiết kế màn hình quản lý thẻ. Mỗi thẻ có một hàng icon: khóa thẻ, đổi mã PIN, xem lịch sử. Tất cả chỉ là icon, và chức năng được giải thích bằng tooltip khi hover.

Khi audit accessibility trước khi ra mắt, đội kiểm thử phát hiện: với screen reader (họ test bằng TalkBack trên Android vì đa số khách hàng dùng điện thoại), mỗi icon chỉ được đọc là "button, button, button" — không có tên. Người khiếm thị hoàn toàn không phân biệt được nút nào là khóa thẻ, nút nào là đổi PIN. Đây là rủi ro nghiêm trọng: bấm nhầm nút khóa thẻ có thể khiến khách hàng không dùng được thẻ giữa lúc cần.

Cách sửa: vì các nút này chỉ có icon, tooltip trở thành tên của nút, nên đội chuyển từ aria-describedby sang aria-labelledby gắn tới đúng id của mỗi tooltip, và thêm aria-hidden="true" vào các <svg>. Sau khi sửa, TalkBack đọc rõ "Khóa thẻ, nút", "Đổi mã PIN, nút". Bài học: với nút chỉ có icon, tooltip không phải là "thông tin thêm" mà là danh tính của nút — phải dùng aria-labelledby.

Tình huống 2 — Sàn thương mại điện tử Đông Nam Á và tooltip "nhấp nháy"

Một sàn TMĐT lớn khu vực Đông Nam Á (tương tự Shopee/Lazada) có tooltip giải thích chính sách "Freeship Extra" khi hover vào icon dấu hỏi cạnh giá. Người dùng phản ánh rằng khi họ cố rê chuột lên tooltip để đọc kỹ đoạn text dài, tooltip biến mất ngay. Với người lớn tuổi hoặc người có run tay, việc này khiến họ gần như không đọc nổi nội dung.

Nguyên nhân kỹ thuật: có một khoảng trống 6px giữa icon và hộp tooltip. Khi con trỏ đi qua khoảng trống đó, trình duyệt kích hoạt mouseleave và ẩn tooltip. Điều này vi phạm điều kiện hoverable của WCAG 1.4.13.

Cách sửa: đội thêm một vùng "cầu nối" trong suốt lấp khoảng trống, và thêm một khoảng trễ nhỏ (delay ~100ms) trước khi ẩn để con trỏ kịp di chuyển sang. Đồng thời họ bổ sung phím Esc để đóng tooltip (điều kiện dismissible). Sau khi sửa, tỷ lệ người dùng thực sự đọc hết nội dung tooltip tăng rõ rệt trong test nội bộ. Bài học: tooltip không chỉ là "hiện text" — nó phải giữ được để người ta đọc, nhất là với người có khó khăn vận động.

Tình huống 3 — SaaS nội bộ và tooltip chứa link

Một công ty SaaS ở Hà Nội làm dashboard quản trị. Họ nhét vào tooltip một cái link "Xem hướng dẫn chi tiết". Trên desktop dùng chuột thì có vẻ ổn. Nhưng người dùng bàn phím thì không tài nào bấm được cái link đó: khi họ Tab ra khỏi nút, tooltip đóng lại, nên không bao giờ Tab tới được link bên trong. Còn screen reader thì đọc link như một phần mô tả rời rạc, gây khó hiểu.

Vấn đề gốc: họ đã dùng sai component. Thứ chứa nội dung tương tác được (link, nút) không phải tooltip mà là toggletip/popover — cần được mở bằng click, giữ focus bên trong, và đóng bằng Esc. Đội đã đổi icon dấu hỏi thành một nút bấm mở popover, và bỏ hẳn hành vi hover. Bài học: nếu bạn thấy mình muốn bỏ link/nút vào tooltip, đó là tín hiệu bạn đang cần một component khác. Tooltip chỉ chứa text tĩnh.

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

Đây là quy trình để làm một tooltip đúng chuẩn:

  • Xác định phần tử điều khiển có tên chưa. Nút có text nhìn thấy được → dùng aria-describedby. Nút chỉ có icon → dùng aria-labelledby. Đây là bước quyết định cả kiến trúc, làm trước tiên.
  • Dùng đúng phần tử điều khiển có thể focus. Luôn là <button> (cho hành động) hoặc <a> (cho điều hướng). Nếu chỉ cần một điểm neo tooltip thuần thông tin trên text, bạn vẫn nên cân nhắc kỹ vì phần tử không tương tác thì người dùng bàn phím khó với tới.
  • Tạo hộp tooltip với role="tooltip" và một id duy nhất. Gắn id này vào aria-describedby/aria-labelledby của nút. Đặt hidden cho trạng thái mặc định.
  • Ẩn icon trang trí khỏi screen reader. Thêm aria-hidden="true" cho <svg> hoặc icon font, để chúng không gây nhiễu khi đọc.
  • Gắn trigger cho cả hai kênh. Khi mouseenter HOẶC focus → hiện tooltip (bỏ hidden). Khi mouseleave HOẶC blur → ẩn tooltip. Nhớ: focus và hover phải song hành.
  • Thêm phím Esc để đóng (dismissible). Lắng nghe keydown; nếu là Escape thì ẩn tooltip mà không bỏ focus khỏi nút.
  • Đảm bảo hoverable. Loại bỏ khoảng trống giữa nút và tooltip, hoặc thêm vùng cầu nối trong suốt, hoặc thêm delay nhỏ trước khi ẩn, để con trỏ di chuyển sang tooltip mà không làm nó biến mất.
  • Kiểm tra tương phản và kích thước chữ. Text tooltip phải đạt tỷ lệ tương phản tối thiểu 4.5:1 với nền của nó, và phải phóng to được cùng phần còn lại của trang.
  • Test đủ ba kênh. Chỉ dùng bàn phím (Tab tới, thấy tooltip, bấm Esc đóng được). Dùng screen reader (nghe tên/mô tả được đọc đúng). Dùng chuột (rê lên tooltip không biến mất).

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

  • Dùng thuộc tính title của HTML làm tooltip. Đây là lỗi phổ biến nhất. title không hiện khi focus bằng bàn phím, không đọc nhất quán trên các screen reader, không đóng được bằng Esc, và không thể tạo kiểu. Đừng dùng title cho thông tin quan trọng. Nó chỉ tạm ổn cho chú thích không thiết yếu.
  • Dùng <div> hoặc <span> làm trigger. Phần tử không focus được thì người dùng bàn phím không bao giờ thấy tooltip. Luôn dùng <button> hay <a>.
  • Chỉ trigger bằng hover, quên focus. Người dùng bàn phím bị bỏ lại. Luôn gắn cả mouseenter và focus.
  • Tooltip tự tắt sau vài giây. Vi phạm điều kiện persistent. Đừng đặt hẹn giờ tự ẩn.
  • Nhét nội dung tương tác vào tooltip. Như tình huống 3, đây là tín hiệu bạn cần popover/toggletip, không phải tooltip.
  • Vừa dùng aria-labelledby vừa để nút có text riêng bị trùng. Nếu dùng aria-labelledby, nội dung được trỏ tới sẽ thay thế hoàn toàn tên nút. Đừng để nút vừa có text vừa aria-labelledby gây đọc lặp.
  • Mẹo: hãy tự hỏi "Người dùng có bắt buộc phải đọc thông tin này để dùng được chức năng không?" Nếu CÓ, đừng giấu nó trong tooltip — cân nhắc hiển thị luôn trên giao diện. Tooltip tốt nhất dành cho thông tin bổ trợ, không thiết yếu.
  • Mẹo: trên thiết bị cảm ứng không có hover. Nếu tooltip quan trọng, hãy đảm bảo có cách khác để truy cập nó trên mobile (ví dụ chuyển sang toggletip mở bằng tap).

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

  • Chẩn đoán component. Lấy 3 tooltip bất kỳ trên một website Việt Nam (ngân hàng, sàn TMĐT, hoặc trang tin). Với mỗi cái, xác định: nút có text hay chỉ icon? Nên dùng aria-describedby hay aria-labelledby? Nó có hiện khi bạn Tab tới không? Có đóng được bằng Esc không?
  • Sửa một tooltip icon-only. Viết lại đoạn HTML cho một nút chỉ có icon bánh răng, sao cho screen reader đọc đúng "Cài đặt, nút". Dùng đúng aria-labelledby, role="tooltip", và aria-hidden cho icon.
  • Kiểm thử hoverable. Mở một trang có tooltip, rê chuột vào để hiện tooltip, rồi cố di chuyển con trỏ lên chính hộp tooltip. Nó có biến mất không? Ghi lại kết quả và đề xuất cách sửa nếu nó nhấp nháy.
  • Kiểm thử ba kênh. Với một tooltip bạn tự dựng, hoàn thành bảng kiểm: (a) Tab tới thấy tooltip? (b) Esc đóng được? (c) Screen reader đọc đúng tên/mô tả? (d) Rê chuột lên tooltip không mất? Cả bốn phải đạt "Có".
  • Ra quyết định thiết kế. Bạn cần chú thích một trường "Mã số thuế" trong form với nội dung dài kèm một link "Xem cách tra cứu". Đây nên là tooltip hay toggletip/popover? Giải thích lý do dựa trên nguyên tắc "tooltip chỉ chứa text tĩnh".

Tóm tắt

Tooltip nhìn thì nhỏ nhưng là bài kiểm tra thực sự cho tư duy inclusive. Điểm cốt lõi cần nhớ: chọn giữa aria-describedby (khi nút đã có tên, tooltip là mô tả bổ sung) và aria-labelledby (khi nút chỉ có icon, tooltip chính là tên). Luôn dùng role="tooltip" cho hộp nội dung, và luôn để tooltip xuất hiện bằng cả focus lẫn hover — không được bỏ rơi người dùng bàn phím. Tuân thủ WCAG 1.4.13: tooltip phải đóng được bằng Esc (dismissible), rê chuột lên được mà không biến mất (hoverable), và giữ lại cho tới khi người dùng chủ động bỏ (persistent).

Ba nguyên tắc bảo vệ bạn khỏi hầu hết lỗi: một là dùng <button>/<a> thật để phần tử focus được; hai là không bao giờ dùng thuộc tính title cho thông tin quan trọng; ba là nếu bạn muốn nhét link hay nút vào trong, đó không còn là tooltip mà là popover — hãy đổi component. Làm đúng những điều này, cái tooltip nhỏ bé của bạn sẽ phục vụ được tất cả mọi người, dù họ dùng chuột, bàn phím hay screen reader.

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