The rule fits in one line: if clicking it changes the URL, it’s a link; if it does something on the current page, it’s a button. “Go to settings” is a link. “Save settings” is a button. “Open the menu” is a button. “Download the PDF” is a link (it’s a navigation to a file URL).
The rule is easy. What makes this an interview question is everything that follows from it: the keyboard behaviour, what screen readers announce, what the browser offers on right-click, how forms react, and the specific bugs you get when someone picks the wrong element because it was easier to style.
What each element gives you for free
An <a> with an href is a navigation primitive. It is focusable with Tab. It activates with Enter. Screen readers announce it as “link”. Users can middle-click or Cmd-click it to open a new tab, right-click to copy the address, drag it to the bookmarks bar, and see its destination in the status bar on hover. Visited links can be styled with :visited. Search engines follow it. All of that comes from one attribute: href.
A <button> is an action primitive. It is focusable with Tab. It activates with Enter and with Space (Space fires on key release, which lets a user cancel by moving focus away before letting go). Screen readers announce it as “button”. It can be disabled, which removes it from the tab order and stops clicks. Inside a form it submits the form unless told otherwise. It has no destination, so none of the open-in-new-tab behaviour exists, and that’s correct, because “Delete comment” in a new tab means nothing.
The keyboard difference matters more than it sounds. Screen reader and keyboard users learn that Space activates buttons and Enter follows links. If something is announced as a button but only responds to Enter, or announced as a link but performs an action, the user has been told something false about the control.
The two classic mistakes
A link pretending to be a button
<a href="#" onclick="openCart(); return false;">Cart (3)</a>
<a href="javascript:void(0)" class="btn">Delete</a>These render and click fine with a mouse. Here is what goes wrong:
- A screen reader announces “link, Cart (3)”, so the user expects a page change and gets a panel opening instead.
- Space scrolls the page instead of activating, because links don’t respond to Space.
- Middle-clicking opens a new tab at
#or a blank tab, a broken experience that confuses even mouse users. - If JavaScript fails or hasn’t loaded yet,
href="#"jumps the page to the top.
A div pretending to be either
<div class="btn" onclick="save()">Save</div>This is worse. The div isn’t focusable, so keyboard users can’t reach it at all. It has no role, so a screen reader reads “Save” as plain text. Making it equivalent to a <button> requires role="button", tabindex="0", a keydown handler for Enter, a keyup handler for Space that also prevents the page scroll, and aria-disabled plus manual click blocking when disabled. That’s five things to remember, and every reimplementation forgets one. The <button> element does all of it already.
The form trap
Once people switch to <button>, they meet the most common surprise in this area:
<form action="/profile" method="post">
<label for="bio">Bio</label>
<textarea id="bio" name="bio"></textarea>
<button onclick="insertTemplate()">Insert template</button>
<button>Save profile</button>
</form>Clicking “Insert template” runs insertTemplate() and then submits the form, reloading the page, because a <button> inside a <form> has type="submit" by default. The fix is to state the type:
<button type="button" onclick="insertTemplate()">Insert template</button>
<button type="submit">Save profile</button>A good habit is to write type on every button, even the submit ones, so the intent is readable without remembering the default. Some teams lint for it.
The submit button also brings a feature that a div or link can’t: pressing Enter inside a text input submits the form through its submit button (implicit submission). Built correctly, the form works before JavaScript loads, and the browser’s own validation runs on submit.
Making them look like each other
Design systems frequently want a link that looks like a primary button (“Start free trial” going to /signup) or a button that looks like a text link (“Show more” inside a paragraph). That’s fine. Appearance and semantics are independent, so pick the element from behaviour and the class from design:
<a class="btn btn-primary" href="/signup">Start free trial</a>
<button type="button" class="btn-link" aria-expanded="false" aria-controls="details">
Show more
</button>.btn {
display: inline-block;
padding: 0.625rem 1rem;
border-radius: 6px;
background: #1d4ed8;
color: #fff;
text-decoration: none;
}
.btn-link {
/* strip the user-agent button styles */
appearance: none;
background: none;
border: 0;
padding: 0;
font: inherit;
color: #1d4ed8;
text-decoration: underline;
cursor: pointer;
}font: inherit matters: buttons don’t inherit font settings by default in most browsers, so without it “Show more” renders in a slightly different typeface and size from the surrounding paragraph. aria-expanded tells screen reader users whether the extra content is currently shown; your script flips it between "false" and "true" when it toggles the section.
The rendered result is a blue filled box that is still a link (right-click shows “Open link in new tab”) and inline underlined text that is still a button (Space activates it, a screen reader says “Show more, button, collapsed”).
Cases that need a moment’s thought
Client-side routing. In a single-page app, clicking “Settings” might not trigger a network navigation, because the router intercepts it. It’s still a link: the URL changes and the user can bookmark it. Frameworks’ <Link> and routerLink components render an <a href> for exactly this reason.
Tabs, menus and dialogs. Opening a dialog is an action, so it’s a button. Tabs that swap panels without changing the URL are buttons with role="tab". If each tab has its own URL (/account/billing, /account/security), they’re links in a <nav>.
Sign out. Signing out changes server state, so it should be a POST, which means a <button> inside a small <form method="post">. A GET link to /logout can be triggered by link prefetching or by an image tag on another site.
Pagination and filters. “Page 2” changes the URL and should be a link, so it works without JavaScript and can be crawled. A filter checkbox that updates results in place is a form control, not a link or a button.
Interview practice
A product card has three clickable things: the product title (goes to the product page), an “Add to cart” control, and a heart icon that saves the product to a wishlist. The current code uses <div onclick> for all three. Which elements would you use, and what else does the heart need?
Answer
The title is a link: <a href="/products/42">Trail Runner 3</a>, because it navigates and users will want to open products in new tabs to compare them. “Add to cart” is a <button type="button">, because it changes state (the cart) without changing the URL. If the card sits inside a form, the type prevents an accidental submit. The heart is also a <button type="button">, but since it has no visible text it needs an accessible name, such as aria-label="Save Trail Runner 3 to wishlist" (including the product name, because a list of twenty cards with twenty identical “Save” buttons is hard to tell apart). It’s a toggle, so it should expose state with aria-pressed="true" or "false", and the SVG icon inside should have aria-hidden="true" so it isn’t read twice. Finally, check that the focus outline is visible on all three, since custom card styles often remove it.