Product Management
Đăng nhập
ESC

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

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

Bài 20 — Buttons vs Links — A11y Difference

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

Nếu bạn hỏi mười designer hoặc developer rằng "sự khác nhau giữa <button> và <a> là gì?", có lẽ tám người sẽ trả lời bằng câu chuyện hình thức: "Link thì màu xanh có gạch chân, còn button thì có nền và bo góc." Đó là câu trả lời về mặt thẩm mỹ. Nhưng đối với accessibility, sự khác biệt giữa button và link không nằm ở lớp sơn bên ngoài — nó nằm ở ý nghĩa (semantic) của phần tử, và ý nghĩa đó quyết định trải nghiệm của hàng triệu người dùng đang lướt web bằng bàn phím, bằng screen reader, hay bằng phần mềm điều khiển giọng nói.

Đây là một trong những lỗi accessibility phổ biến nhất mà tôi thấy khi đi audit các website Việt Nam: một cái nút "Đặt hàng" trông rất đẹp, có hiệu ứng hover mượt mà, nhưng khi bạn dùng phím Tab để đi tới nó thì... không có gì xảy ra. Hoặc ngược lại, một đường dẫn "Xem chi tiết sản phẩm" lại được lập trình như một cái button, khiến screen reader đọc sai và người dùng khiếm thị không biết là nó sẽ chuyển trang hay thực hiện hành động.

Bài này sẽ giúp bạn phân biệt rạch ròi hai phần tử tưởng chừng giống nhau này, hiểu vì sao việc dùng đúng phần tử lại là nền tảng của accessibility, và biết cách sửa những trường hợp đã lỡ dùng sai. Đây là kiến thức tưởng đơn giản nhưng ảnh hưởng cực lớn — vì button và link xuất hiện ở khắp mọi trang, mọi component.

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

Sự khác biệt về semantic — hành động vs điều hướng

Nguyên tắc vàng để phân biệt rất ngắn gọn:

  • <button> dùng khi phần tử thực hiện một hành động (action) ngay tại trang hiện tại: submit form, mở modal, đóng popup, bật/tắt menu, thêm sản phẩm vào giỏ hàng, phát video, xóa một dòng dữ liệu.
  • <a href="..."> dùng khi phần tử điều hướng (navigation) — đưa người dùng tới một địa chỉ (URL) khác: sang trang khác, tới một mục trên cùng trang (anchor), tải về một file, mở một email mailto:.
Một câu hỏi kiểm tra nhanh mà tôi hay dạy học viên: "Sau khi bấm vào phần tử này, URL trên thanh địa chỉ có thay đổi không?" Nếu có — dùng link. Nếu không, mà chỉ có gì đó thay đổi trên chính trang hiện tại — dùng button.

Ví dụ, "Đăng nhập" mở ra một modal ngay trên trang → button. "Đăng nhập" đưa bạn tới trang /dang-nhap → link. Cùng một chữ, nhưng bản chất khác nhau hoàn toàn.

Vì sao semantic lại quan trọng đến vậy với accessibility

Trình duyệt xây dựng một cấu trúc song song với DOM gọi là accessibility tree — đây chính là thứ mà screen reader "đọc". Khi bạn dùng <button>, trình duyệt tự động gán cho nó role = button, và screen reader sẽ đọc là "Đặt hàng, nút" (button). Khi bạn dùng <a href>, nó nhận role = link, và screen reader đọc "Xem chi tiết, liên kết" (link).

Sự khác biệt này báo cho người dùng khiếm thị biết điều gì sắp xảy ra trước khi họ bấm. Nghe thấy "nút" thì họ hiểu sẽ có một hành động; nghe thấy "liên kết" thì họ chuẩn bị tinh thần trang sẽ chuyển. Nếu bạn dùng sai, bạn đang nói dối họ về hệ quả của thao tác — đó là một sự vi phạm lòng tin rất khó chịu.

Ba khác biệt về hành vi bàn phím và tương tác

Khi bạn dùng đúng phần tử HTML gốc (native element), trình duyệt tặng miễn phí cho bạn một loạt hành vi mà nếu tự làm sẽ rất tốn công:

  • Focusable tự nhiên — Cả <button> và <a href> đều tự động nằm trong thứ tự Tab, không cần thêm tabindex. Một thẻ <div> giả làm nút thì mặc định KHÔNG focus được.
  • Kích hoạt bằng phím khác nhau — Đây là điểm cực kỳ quan trọng và ít người biết:
- <button> kích hoạt được bằng cả phím Enter và phím Space (dấu cách). - <a href> kích hoạt bằng phím Enter (Space thường dùng để cuộn trang). Người dùng khiếm thị có thói quen bấm Space để "bấm nút" và Enter để "đi theo link". Nếu bạn làm một cái <div onclick> giả làm nút, cả Enter lẫn Space đều không hoạt động, trừ khi bạn tự viết code xử lý — mà 90% trường hợp người ta quên viết.

  • Menu ngữ cảnh và hành vi hệ điều hành — Link cho phép "Mở trong tab mới", "Sao chép địa chỉ liên kết" khi bấm chuột phải. Button thì không. Đây là lý do người dùng ghét khi một "link" thực chất là button — họ không thể Ctrl+Click để mở tab mới.

role="button" không phải là <button>

Bạn có thể "vá" một thẻ <div> bằng role="button" để screen reader đọc nó là nút. Nhưng role chỉ sửa được phần ngữ nghĩa được đọc lên, còn hành vi thì không tự có: bạn vẫn phải tự thêm tabindex="0", tự viết trình xử lý cho cả Enter và Space, tự xử lý trạng thái disabled. Dùng <button> gốc là bạn có tất cả những thứ đó ngay lập tức. Nguyên tắc số 1 của ARIA cũng nói vậy: "Đừng dùng ARIA nếu một phần tử HTML gốc đã làm được việc đó."

Tình huống thực tế

Ví dụ 1 — Sàn thương mại điện tử và cái nút "Thêm vào giỏ" giả

Một startup thương mại điện tử ở TP.HCM (tạm gọi là ShopViet) thuê tôi audit trang sản phẩm trước mùa sale 12/12. Đội frontend của họ dùng một component "nút" như thế này:

<div class="btn-add" onclick="addToCart(id)">Thêm vào giỏ</div>

Trông đẹp, hover mượt, ai dùng chuột đều thấy ổn. Nhưng khi tôi tắt chuột và chỉ dùng bàn phím: phím Tab lướt qua toàn bộ các nút "Thêm vào giỏ" như thể chúng vô hình. Một khách hàng khiếm thị hoàn toàn không thể mua hàng.

Chúng tôi ước lượng: trong đợt sale, khoảng 3–4% traffic đến từ người dùng có nhu cầu hỗ trợ tiếp cận (theo tỷ lệ khuyết tật chung ~7% dân số Việt Nam, một phần trong đó dùng công nghệ hỗ trợ). Với 200.000 lượt truy cập dự kiến trong ngày sale, con số người bị chặn hoàn toàn khỏi hành động mua là không hề nhỏ.

Cách sửa đơn giản đến bất ngờ — đổi <div> thành <button>:

<button type="button" class="btn-add" onclick="addToCart(id)">Thêm vào giỏ</button>

Chỉ một dòng, và ngay lập tức: nút focus được bằng Tab, kích hoạt được bằng Enter và Space, screen reader đọc "Thêm vào giỏ, nút". Bài học: phần lớn lỗi accessibility về button không cần một dự án lớn để sửa — chỉ cần dùng đúng thẻ HTML ngay từ đầu.

Ví dụ 2 — Ngân hàng số và "link" chuyển tiền nguy hiểm

Một ví điện tử/ngân hàng số tại Đông Nam Á có một luồng chuyển tiền. Ở bước xác nhận cuối, nút "Chuyển tiền" được code là:

<a href="#" onclick="submitTransfer()">Chuyển ngay</a>

Vấn đề rất tế nhị. Người dùng screen reader nghe thấy "Chuyển ngay, liên kết" — họ tưởng đây là link điều hướng, nên có người theo thói quen Ctrl+Click để mở trong tab mới nhằm "xem trước". Kết quả: href="#" khiến trang nhảy về đầu, hàm submit chạy trong ngữ cảnh không mong muốn, và trong một số trường hợp giao dịch bị kích hoạt hai lần. Một hành động tài chính nhạy cảm mà lại được gắn nhãn "link" gây ra rủi ro thật.

Chúng tôi chuyển nó thành button đúng nghĩa:

<button type="button" onclick="submitTransfer()">Chuyển ngay</button>

Ngay lập tức screen reader đọc "Chuyển ngay, nút", không còn hành vi "mở tab mới", và Space/Enter đều hoạt động nhất quán. Bài học: với các hành động không thể hoàn tác (chuyển tiền, xóa, đặt đơn), việc gắn đúng nhãn "nút" không chỉ là accessibility mà còn là an toàn cho người dùng — nó truyền đạt đúng bản chất của thao tác.

Ví dụ 3 — Blog công ty và cái "nút Đọc thêm" thực ra là link

Ngược lại với hai ví dụ trên, một trang tin tức nội bộ của một công ty công nghệ Hà Nội lại lạm dụng button. Mỗi bài viết có một "nút" Đọc thêm:

<button onclick="location.href='/bai-viet/123'">Đọc thêm</button>

Về mặt hình thức thì chạy được. Nhưng đây rõ ràng là điều hướng — nó đưa người dùng sang URL khác. Hệ quả:

  • Người dùng không Ctrl+Click để mở bài viết trong tab mới được.
  • Screen reader đọc "Đọc thêm, nút", trong khi nó thực sự là một liên kết — gây hiểu nhầm.
  • Công cụ tìm kiếm (SEO crawler) và các trình đọc "danh sách tất cả link trên trang" của screen reader bỏ sót nó, vì nó không phải link thật.
Sửa lại đúng bản chất:

<a href="/bai-viet/123">Đọc thêm</a>

Bài học: dùng sai theo chiều ngược lại (button làm việc của link) cũng tai hại không kém. Hãy để hành động → button, điều hướng → link, đừng đảo lộn chỉ vì lý do tạo kiểu (styling).

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

Đây là quy trình tôi khuyên bạn áp dụng mỗi khi tạo hoặc review một phần tử tương tác:

Bước 1 — Đặt câu hỏi bản chất. "Phần tử này làm gì khi được kích hoạt?" Nếu nó đưa người dùng tới một URL/địa chỉ khác → link. Nếu nó thực hiện thay đổi tại chỗ (mở/đóng/gửi/toggle) → button.

Bước 2 — Kiểm tra bằng phép thử URL. Sau khi bấm, thanh địa chỉ có đổi không? Đổi → link. Không đổi → button.

Bước 3 — Chọn đúng phần tử HTML gốc. Ưu tiên tuyệt đối <button> hoặc <a href>. Tránh <div>/<span> gắn onclick. Với button, nhớ đặt type rõ ràng:

  • type="submit" — nếu nút submit form.
  • type="button" — nếu nút làm việc khác (mở modal, toggle...). Nếu quên type bên trong <form>, mặc định nó thành submit và có thể vô tình gửi form.
Bước 4 — Link phải có href thật. Một link tốt luôn có href trỏ tới địa chỉ thật, ngay cả khi bạn dùng JavaScript để can thiệp. Tránh href="#" hay href="javascript:void(0)". Nếu chưa có đích đến, có lẽ nó không phải là link.

Bước 5 — Đừng ép button trông giống link và ngược lại một cách gây nhầm. Bạn hoàn toàn có thể tạo kiểu (CSS) cho một <button> trông như một liên kết văn bản, hoặc cho một <a> trông như một nút bấm nổi khối. Điều đó ổn về mặt kỹ thuật — miễn là bản chất semantic khớp với hành vi. Hãy để CSS phục vụ thiết kế, còn thẻ HTML phục vụ ý nghĩa.

Bước 6 — Test bằng bàn phím. Nhấn Tab để tới phần tử. Kiểm tra: nó có nhận focus không? Với button, thử cả Enter và Space. Với link, thử Enter. Nếu bất kỳ điều gì không hoạt động, bạn đang dùng sai phần tử hoặc thiếu code.

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

Lỗi 1: <div onclick> giả làm nút. Đây là "quán quân" của các lỗi accessibility. Nó không focus được, không có bàn phím, screen reader không nhận diện. Mẹo: đặt một quy tắc trong đội — "không được gắn onclick lên <div> hoặc <span>". Nếu ESLint hoặc linter của bạn hỗ trợ, bật rule cảnh báo điều này.

Lỗi 2: <a href="#"> cho hành động. Link "rỗng" gây nhảy trang, gây hành vi mở-tab-mới không mong muốn. Mẹo: nếu bạn thấy href="#", gần như chắc chắn bạn nên dùng <button>.

Lỗi 3: Quên type="button" bên trong form. Một cái nút "Xóa dòng" bên trong <form> mà thiếu type sẽ submit cả form khi bấm. Mẹo: luôn ghi rõ type.

Lỗi 4: Nhồi cả link và button lồng vào nhau. Ví dụ đặt một <button> bên trong một <a>, hoặc một link bên trong button. HTML không cho phép lồng phần tử tương tác vào nhau — nó gây hành vi khó lường cho cả chuột lẫn screen reader. Mỗi phần tử tương tác phải độc lập.

Lỗi 5: Card "cả khối bấm được" nhưng vẫn có nút bên trong. Một thẻ sản phẩm mà cả khối là link, bên trong lại có nút "Yêu thích" — dễ thành link lồng button. Mẹo: dùng kỹ thuật một link phủ lên (stretched link) hoặc gom lại thành cấu trúc rõ ràng, tránh lồng.

Mẹo vàng: Khi phân vân, hãy nhớ câu thần chú — "Link đưa tôi đi đâu đó. Button làm gì đó cho tôi." Chỉ cần thuộc câu này, bạn đã tránh được đa số lỗi.

Mẹo về icon-only button: Nút chỉ có icon (ví dụ nút X đóng, icon giỏ hàng) vẫn phải là <button> và cần nhãn cho screen reader qua aria-label="Đóng", vì bản thân icon không có văn bản để đọc. Đừng để một nút icon "câm".

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

  • Audit nhanh một trang thật. Mở một website thương mại điện tử Việt Nam bất kỳ (Tiki, Shopee, hoặc trang của công ty bạn). Dùng phím Tab để đi qua trang chi tiết sản phẩm. Ghi lại: có phần tử tương tác nào bị Tab bỏ qua không? Nút "Thêm vào giỏ" có focus được và bấm được bằng Space không? Với mỗi phần tử, tự trả lời "đây nên là button hay link?".
  • Phân loại 10 tình huống. Với mỗi tình huống sau, ghi "button" hay "link": (a) Đăng xuất, (b) Xem hồ sơ người dùng ở trang khác, (c) Mở popup chọn ngày, (d) Tải hóa đơn PDF, (e) Bật/tắt chế độ tối, (f) Chuyển tới mục "Điều khoản" cuối trang, (g) Gửi bình luận, (h) Xóa ảnh đã upload, (i) Sang trang tiếp theo của danh sách, (j) Phát/tạm dừng video. Tự chấm và giải thích lý do.
  • Refactor code. Cho đoạn sau, hãy viết lại cho đúng semantic và giải thích:
   <div class="cta" onclick="openSignupModal()">Đăng ký ngay</div>
   <a href="#" onclick="deleteItem(5)">Xóa</a>
   <button onclick="location.href='/lien-he'">Liên hệ</button>
   
  • Kiểm thử bàn phím. Với một component bạn đang làm, tắt chuột và chỉ dùng bàn phím trong 5 phút. Ghi lại mọi chỗ bạn "kẹt". Đây là bài tập khiến hầu hết học viên "ngộ ra" giá trị của việc dùng đúng phần tử.

Tóm tắt

Sự khác biệt giữa <button> và <a> không phải chuyện thẩm mỹ mà là chuyện ý nghĩa: button là để thực hiện hành động ngay tại trang, link là để điều hướng tới một địa chỉ khác. Phép thử đơn giản nhất là hỏi "sau khi bấm, URL có đổi không?" — đổi thì dùng link, không đổi thì dùng button.

Dùng đúng phần tử HTML gốc mang lại miễn phí cả một hệ sinh thái hành vi accessibility: tự động focus được bằng Tab, kích hoạt đúng phím (button nhận cả Enter và Space, link nhận Enter), được screen reader đọc đúng vai trò, và hỗ trợ đúng menu ngữ cảnh của hệ điều hành. Ngược lại, <div onclick> giả làm nút hay <a href="#"> làm hành động là những lỗi phổ biến, có thể chặn hoàn toàn người dùng khỏi các thao tác quan trọng như mua hàng hay chuyển tiền — như ba tình huống thực tế đã cho thấy.

Hãy ghi nhớ câu thần chú: "Link đưa tôi đi đâu đó. Button làm gì đó cho tôi." Chọn đúng phần tử ngay từ đầu là cách rẻ nhất, hiệu quả nhất để nền tảng accessibility của bạn vững chắc — và đó cũng là bước đệm cho những bài tiếp theo về các component phức tạp hơn như modal, tabs hay dropdown.

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