JavaScript turned static documents into software. Single-page apps feel instant. No full page reloads, no flickering white screens. But that speed comes with a cost that many teams ignore: the basic machinery of the web starts to rot. Navigation becomes brittle. Search engines struggle to follow paths. Screen readers get lost. And users find themselves trapped in interfaces that look like websites but behave like broken desktop applications.

The culprit is usually a div with an onClick handler.

A div carries no semantic meaning. It is a box. When you strap a click handler onto it and use it to route users to a new view, you are asking the browser to treat a cardboard box like a door. The browser refuses. So does every tool built around the browser.

Screen readers do not announce a div as a link or a button. They skip over it, or they read it as plain text. A user navigating by voice cannot target it. A search engine crawler, scanning your page for discoverable URLs, sees nothing to follow. Your route might as well not exist.

Worse, you lose the behaviors users already know. A real link lets someone right-click to open in a new tab, bookmark the destination, or copy the address to share. Keyboard users expect to hit Tab to reach it and Enter to open it. A div offers none of this. Even if you bolt on tabIndex and role="link" and keyboard listeners, you are rebuilding, poorly, what the browser gives you for free. And you will forget an edge case. You always do.

Use Anchors for Destinations, Buttons for Actions

HTML already solved this. The confusion starts because both elements look clickable, so developers treat them as interchangeable. They are not.

Use an <a> tag when you want to move the user to a new URL. Not a simulated view swap, not a state change, but an actual location. The href attribute should contain a real address:

<a href="/docs">Documentation</a>

That is it. If the user is going somewhere, use a link.

Use a <button> when something happens on the current page. Buttons are for actions such as:

  • Opening a modal
  • Submitting a form
  • Saving settings
  • Toggling a menu

Links are for destinations. Buttons are for actions. Mixing the two muddles your interface and breaks user expectations.

Let the Browser Do Its Job

Modern browsers are the result of decades of evolution and standardization. They handle security, history, prefetching, and accessibility better than your hand-rolled JavaScript ever will.

A genuine anchor tag automatically feeds the browser history stack. It works with the native context menu. It participates in the browser's built-in prefetching algorithms when the user hovers or focuses it, making your app feel faster without you writing a line of code. It respects the user's preferences for opening links. It cooperates with password managers, with translation tools, with reader modes.

When you replace that with a JavaScript navigation function, you opt out of all of it. You are not just losing features; you are forcing users to abandon habits they have built across every other site on the internet. That is not a technical decision. It is a hostile user experience.

Check What Your Framework Actually Renders

React Router, Vue Router, Next.js Link components, SvelteKit. These tools make client-side routing feel effortless. But abstraction breeds mistakes.

Inspect your DOM. Open the browser's developer tools and look at the elements your framework emits. A <Link> component should render as a real <a> tag with a valid href attribute in the final HTML. If it renders as a span, a div, or anything else without a proper href, your abstraction has failed you. Fix the component. Override the default. Use the passHref prop or the framework equivalent. Do not trust the framework to get it right without verification.

This matters for hydration mismatches too. If the server renders a link and the client hydrates it into a non-link, you create accessibility bugs that are hard to trace because the HTML looks correct in your source code but wrong in the live DOM.

Ban Fake Destinations

There is a pattern that refuses to die: href="javascript:void(0)". Developers use it when they want something that looks like a link but acts like a button, usually because they do not want to style a button or because an older codebase demands it.

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