JavaScript đã biến các tài liệu tĩnh thành phần mềm. Các ứng dụng đơn trang (single-page apps) mang lại cảm giác tức thì. Không tải lại toàn bộ trang, không có hiện tượng màn hình trắng nhấp nháy. Nhưng tốc độ đó đi kèm với một cái giá mà nhiều đội ngũ thường bỏ qua: bộ máy cơ bản của web bắt đầu bị mục nát. Điều hướng trở nên mong manh. Các công cụ tìm kiếm gặp khó khăn khi theo dõi các đường dẫn. Trình đọc màn hình (screen readers) bị lạc lối. Và người dùng thấy mình bị mắc kẹt trong những giao diện trông giống như trang web nhưng lại hoạt động như những ứng dụng máy tính bị lỗi.
Thủ phạm thường là một thẻ div với trình xử lý onClick.
Ngừng sử dụng Div làm Liên kết
Một thẻ div không mang ý nghĩa ngữ nghĩa (semantic meaning). Nó chỉ là một cái hộp. Khi bạn gắn một trình xử lý sự kiện click vào nó và dùng nó để điều hướng người dùng đến một chế độ xem mới, bạn đang yêu cầu trình duyệt đối xử với một chiếc hộp các-tông như một cánh cửa. Trình duyệt sẽ từ chối. Và mọi công cụ được xây dựng xung quanh trình duyệt cũng vậy.
Trình đọc màn hình không thông báo một thẻ div là một liên kết hay một nút bấm. Chúng sẽ bỏ qua nó, hoặc đọc nó như văn bản thuần túy. Một người dùng điều hướng bằng giọng nói không thể nhắm mục tiêu vào nó. Một trình thu thập dữ liệu của công cụ tìm kiếm, khi quét trang của bạn để tìm các URL có thể khám phá, sẽ không thấy gì để theo dõi. Đường dẫn của bạn coi như không tồn tại.
Tệ hơn nữa, bạn làm mất đi những hành vi mà người dùng vốn đã quen thuộc. Một liên kết thực thụ cho phép ai đó nhấp chuột phải để mở trong tab mới, đánh dấu trang (bookmark) đích đến, hoặc sao chép địa chỉ để chia sẻ. Người dùng bàn phím mong đợi nhấn phím Tab để đến đó và phím Enter để mở nó. Một thẻ div không cung cấp bất kỳ điều nào trong số này. Ngay cả khi bạn chắp vá thêm tabIndex, role="link" và các trình lắng nghe bàn phím, bạn đang tái xây dựng một cách vụng về những gì trình duyệt đã cung cấp miễn phí cho bạn. Và bạn sẽ quên mất một trường hợp biên (edge case). Bạn luôn luôn quên.
Sử dụng Anchor cho Đích đến, Button cho Hành động
HTML đã giải quyết vấn đề này rồi. Sự nhầm lẫn bắt đầu vì cả hai phần tử đều trông có vẻ có thể nhấp vào được, nên các lập trình viên coi chúng là có thể thay thế cho nhau. Thực tế không phải vậy.
Sử dụng thẻ <a> khi bạn muốn chuyển người dùng đến một URL mới. Không phải là một sự thay đổi chế độ xem mô phỏng, không phải là thay đổi trạng thái (state), mà là một vị trí thực sự. Thuộc tính href nên chứa một địa chỉ thực:
<a href="/docs">Documentation</a>
Chỉ vậy thôi. Nếu người dùng đang đi đến một nơi nào đó, hãy dùng một liên kết.
Sử dụng <button> khi có một điều gì đó xảy ra trên trang hiện tại. Nút bấm dành cho các hành động như:
- Mở một modal
- Gửi một biểu mẫu (form)
- Lưu cài đặt
- Bật/tắt một menu
Liên kết dành cho đích đến. Nút bấm dành cho hành động. Việc trộn lẫn cả hai sẽ làm rối loạn giao diện và phá vỡ kỳ vọng của người dùng.
Hãy để Trình duyệt làm việc của nó
Các trình duyệt hiện đại là kết quả của nhiều thập kỷ tiến hóa và tiêu chuẩn hóa. Chúng xử lý bảo mật, lịch sử, tải trước (prefetching) và khả năng truy cập (accessibility) tốt hơn bất kỳ đoạn mã JavaScript tự viết nào của bạn.
Một thẻ anchor thực thụ sẽ tự động nạp vào ngăn xếp lịch sử (history stack) của trình duyệt. Nó hoạt động với menu ngữ cảnh (context menu) gốc. Nó tham gia vào các thuật toán tải trước tích hợp sẵn của trình duyệt khi người dùng di chuột qua hoặc tập trung (focus) vào nó, giúp ứng dụng của bạn có cảm giác nhanh hơn mà không cần viết một dòng mã nào. Nó tôn trọng các tùy chọn của người dùng khi mở liên kết. Nó hợp tác với các trình quản lý mật khẩu, các công cụ dịch thuật và các chế độ đọc (reader modes).
Khi bạn thay thế điều đó bằng một hàm điều hướng JavaScript, bạn đang từ bỏ tất cả những lợi ích này. Bạn không chỉ mất đi các tính năng; bạn đang buộc người dùng phải từ bỏ những thói quen mà họ đã xây dựng trên mọi trang web khác trên internet. Đó không phải là một quyết định kỹ thuật. Đó là một trải nghiệm người dùng tồi tệ.
Kiểm tra những gì Framework của bạn thực sự Render
React Router, Vue Router, các component Link của Next.js, SvelteKit. Những công cụ này giúp việc điều hướng phía client (client-side routing) trở nên dễ dàng. Nhưng sự trừu tượng hóa (abstraction) thường nảy sinh sai lầm.
Hãy kiểm tra DOM của bạn. Mở công cụ dành cho nhà phát triển (developer tools) của trình duyệt và xem các phần tử mà framework của bạn tạo ra. Một component <Link> nên được render thành một thẻ <a> thực thụ với thuộc tính href hợp lệ trong mã HTML cuối cùng. Nếu nó được render thành một thẻ <span>, <div>, hoặc bất kỳ thứ gì khác mà không có href phù hợp, thì sự trừu tượng hóa của bạn đã thất bại. Hãy sửa component đó. Ghi đè (override) thiết lập mặc định. Sử dụng prop passHref hoặc các thuộc tính tương đương của framework. Đừng tin tưởng framework sẽ làm đúng mà không có sự xác minh.
Điều này cũng quan trọng đối với các lỗi không khớp khi hydration (hydration mismatches). Nếu máy chủ render một liên kết và phía client hydrate nó thành một thứ không phải liên kết, bạn sẽ tạo ra các lỗi về khả năng truy cập (accessibility bugs) rất khó truy vết vì mã HTML trông có vẻ đúng trong mã nguồn nhưng lại sai trong DOM thực tế.
Cấm các Đích đến Giả
Có một khuôn mẫu (pattern) vẫn chưa chịu biến mất: href="javascript:void(0)". Các lập trình viên sử dụng nó khi họ muốn một thứ gì đó trông giống như một liên kết nhưng lại hoạt động như một nút bấm, thường là vì họ không muốn định dạng (style) một nút bấm hoặc vì một mã nguồn cũ yêu cầu như vậy.
Stop. This is not a URL. It gives the browser no destination. It pollutes the history stack with unusable states. It breaks browser history and accessibility. It is a trap. If you need click behavior without navigation, you need a <button>. Style it to look however you want. CSS does not care whether the element is a button or a link. Your users do.
Write Text That Explains Where the User Is Going
The words inside your link matter. Screen reader users often pull up a list of every link on the page to scan quickly. If your links all say "Read more" or "Click here," that list becomes useless noise.
Be specific. Compare these:
- Bad:
<a href="/security/api-guide">Read more</a> - Good:
<a href="/security/api-guide">Read the API security guide</a>
The second tells the user exactly what they will find. It gives search engines context about the destination page. It makes your link list navigable. Descriptive link text is one of the cheapest accessibility wins you can earn.
Test Like You Mean It
Architecture means nothing if you do not verify it.
First, test your keyboard flow. Unplug your mouse. Tab through every interactive element on your site. Every genuine link must show a visible focus outline, not a subtle glow that disappears against your background, but a clear ring that a tired eye can spot. Press Enter. It must activate the link. If Tab skips an element, or if Enter does nothing, you have a bug.
Second, test your routes at the server level. Client-side routing is a thin veneer. If a user bookmarks /dashboard/reports and returns tomorrow, or hits refresh, your server must know how to serve that page. Configure your reverse proxy or your server framework to fall back to your application shell for unknown paths, or serve the correct HTML directly. A dead 404 on refresh is not a minor bug. It is a broken promise.
JavaScript is a powerful layer
