avatarAquaticat

Link vs Button — Tricky Questions

<a> and <button> look interchangeable: both are clickable, both can run JavaScript, both can be styled to look like whichever the designer prefers. The actual choice between them is a semantic and security decision, not a visual one. <a> means "go somewhere I can bookmark and share." <button> means "do something right now on this page." Get it wrong and you break the back button, break middle-click, break screen readers, or — in the worst case — open a hole that lets any page on the internet silently trigger destructive actions against your users.

The scenarios below are the ones that trip people up. A few have a clean right answer, a few have more than one right answer, and some come down to how the feature actually behaves at runtime rather than how it looks in a mockup. Pick what you'd reach for — the explanation reveals as soon as you click, and you can change your pick at any time. Most questions expect a single choice; one question (the shuffle toggle) accepts multiple answers and uses checkboxes instead.

A "Sign out" option in the top navigation bar.

If you write this as <a href="/logout">, any other website can sign your users out without them knowing. They just put <img src="/logout"> on their page — when your user visits, their browser loads that URL and the sign-out happens. This is called CSRF (Cross-Site Request Forgery): one site forging a request to another. Actions that change something on the server should never happen from a plain URL request (called a GET request).
Signing out changes something important — it ends the user's session. Actions like that should send a POST or DELETE request (the HTTP methods that mean "do something on the server," as opposed to GET, which means "show me something"). A <button> inside a <form method="post"> does exactly this. A link can't — links can only send GET requests.
You don't need two controls here. As long as one of them is a link pointing at /logout, the CSRF problem is still there — any other site can trigger it with an <img> tag.
There's no better-fit element for sign-out than a button inside a form. Other inputs like <input type="checkbox"> or <select> are for picking a value or toggling a state, not for triggering a one-time action.
This rule doesn't change based on surrounding context. Sign-out always needs to be a button that submits a form, never a link.

A "Shuffle" toggle in a music player that turns random playback on and off.

A link is for sending the user to a new URL. Turning shuffle on doesn't change the URL and doesn't reload the page — it just flips an on/off switch inside the player. No navigation means no link.
A <button> works for a toggle, but a plain button can't tell a screen reader whether shuffle is currently on or off. Add aria-pressed="true" when shuffle is on and aria-pressed="false" when it's off. aria-pressed is a WAI-ARIA attribute — ARIA is a set of extra HTML attributes that help assistive tech like screen readers understand the page — and it announces the toggle state out loud to users who can't see the visual change.
Only one control toggles shuffle. Showing two would confuse users and assistive tech — they wouldn't know which one to click or how two controls are supposed to work together.
Another good fit is <input type="checkbox">. A checkbox is designed for exactly this kind of two-state on/off choice. You can style it to look like a switch or a toggle button with CSS — the visual style is independent of the underlying element. Screen readers announce the checked/unchecked state automatically, so you don't need any extra ARIA attributes.
Shuffle is always an on/off state regardless of the music player's layout. The answer doesn't change with context.

A "Name" column header in a data table that sorts rows when clicked, with no URL change.

A link sends the user somewhere — it changes the URL or jumps to a spot on the page. In this scenario the URL stays the same and the table just re-orders its existing rows. There's no destination for a link to point at.
Clicking the header triggers an action (sorting) that rearranges what's already on the screen. A <button> fits that exactly. Put the <button> inside the <th> (the table header cell), and add aria-sort to the <th>. aria-sort is a WAI-ARIA attribute — ARIA adds extra information for assistive tech like screen readers — that tells them whether the column is currently sorted "ascending", "descending", or "none". Without it, a sighted user sees a little arrow icon but a screen reader user gets no cue at all.
A column header has one clickable area: the header itself. Adding a second control next to it would clutter the layout without giving the user anything new to do.
No other built-in HTML element fits sorting better than a button. A <select> could work if you wanted the user to pick a sort column from a dropdown, but clicking the header itself is a button-shaped action.
The scenario pins down that the URL doesn't change, which removes the ambiguity. Without a URL to link to, the answer has to be button.

A "Learn more about our pricing" option next to a plan selector on a signup form.

Right if clicking this takes the user to a full pricing page like /pricing. That's exactly what a link with an href does. Wrong if clicking it opens a little popup or tooltip on the same page without leaving.
Right if clicking opens a tooltip or popover (a small floating box of extra info) on the same page. Wrong if clicking takes the user to a separate pricing page — a button has no URL to go to.
A single well-chosen control is enough. Two "learn more" affordances side by side would just clutter the form without teaching the user anything new.
Worth knowing about: the <details> element. Combined with a <summary> inside it, <details> creates a native expandable section — click the summary, and the hidden content unfolds inline. No JavaScript needed. It can fit a "learn more" pattern when the extra info is short and belongs right next to the question. But for the classic link-vs-button call in this scenario, the answer really does depend on what the click does.
The right element depends entirely on what happens when the user clicks. Opens a popover on this page → button. Takes the user to a separate pricing page → link. "Learn more" is notoriously ambiguous — a clearer label like "Show pricing details" or "View full pricing" would tell the user which behavior to expect before clicking.

A "Continue to payment" option at the bottom of the delivery address step in a multi-step checkout.

A link can't send the user's form data to the server. If they right-clicked and chose "Open in new tab," the browser would just visit the next URL without the address they typed — the data would be lost. Links only know how to fetch a URL (a GET request); they can't carry form contents.
Even though it feels like moving to the next page, the button is doing something first: sending the delivery address to the server. A <button type="submit"> inside the <form> submits the form data (via POST), and the server responds by showing the next step. The URL changing afterward is a result of the submission, not the point of the button.
One submit button per form step. Two buttons that both submit the same form would let the user click both by accident — and a double submit on a checkout can mean being charged twice.
A form-submit <button> is the clearest HTML element for advancing a form. <input type="submit"> is older syntax that does the same thing, but <button> is more flexible because you can put icons or other content inside it.
A multi-step form always needs to send the current step's data to the server before moving on. The right element doesn't change based on layout or styling.

You want a whole product card to link to /product/42, with a "Save for later" button inside the card that saves to a wishlist without navigating away.

Wrapping the whole card in <a href="/product/42"> seems to cover the navigation, but it breaks the Save button. The HTML spec forbids putting interactive elements (like <button>) inside other interactive elements (like <a>). Browsers will try to repair the invalid markup in unpredictable ways, and screen readers announce the mess inconsistently.
A button alone can't navigate to /product/42 — buttons have no href. The Save action does need a button, but we also need something to handle the page-level link to the product detail page.
You need both, but not nested. Use the "stretched-link" pattern: put a plain <a href="/product/42"> on just the product title, then use CSS to make that link cover the whole card. The trick: give the card position: relative, and put ::after { content: ""; position: absolute; inset: 0; } on the link. The ::after is a pseudo-element (a CSS-generated fake element), and setting inset: 0 stretches it to fill the card, so clicking anywhere on the card triggers the link. The Save button sits on top with its own z-index and stays independently clickable.
No single HTML element can both navigate to a URL and trigger a non-navigating action at the same time. You genuinely need a link element and a button element — the question is just how to arrange them so they don't nest.
The no-nesting rule is universal: it's an HTML spec rule, not a styling choice. The stretched-link solution works in any card layout, so the answer doesn't depend on context.

A "Back to top" control that scrolls the user to the top of a long page.

<a href="#top"> works without any JavaScript. When the user clicks it, the browser scrolls to the top of the page because #top is a special URL fragment — if there's no element on the page with id="top", the browser falls back to scrolling to the top of the document. Screen readers announce it as a link that jumps somewhere, which is exactly what the user expects.
A button would need JavaScript (like window.scrollTo(0, 0)) to do the scroll. You'd be rebuilding something the browser already does for free using a link and a URL fragment.
One control is all the user needs. A second "back to top" would just add visual noise without giving the user any new ability.
No other built-in HTML element does fragment navigation as cleanly as <a>. You could rig something up with pure CSS (like scroll-margin or container queries), but for a simple "go to top" control the link is the simplest and most accessible choice.
The #top fragment trick works in every browser in every context. No surrounding layout or styling changes this answer.

A "Clear form" option that resets all fields to empty.

Clearing the form doesn't change the URL — the user stays on the same page. Links are for navigation, not for resetting inputs.
<button type="reset"> is built into HTML. Put it inside a <form>, and clicking it automatically resets every input in that form back to its default value. No JavaScript needed. One real-world warning: most designers advise against including a Clear button at all. It sits right next to Submit, users click it by accident, lose their work, and get furious. If you must include one, place it far from Submit and style it to look less prominent (smaller, less bold).
A form has one Clear at most. More than one just multiplies the chance the user hits one by accident.
No other built-in HTML element fits "reset every field in this form" as directly as <button type="reset">. <input type="reset"> is the older syntax and does exactly the same thing — either one works.
Reset behavior is the same regardless of which form, layout, or styling. The answer doesn't change with context.

A tab interface switching between "Overview", "Reviews", and "Specs" panels — with no URL changes.

Without URLs there's nothing for a link to point at. If each tab loaded a real URL like /reviews or #reviews, using links with a tab role would be reasonable. But the scenario says "no URL changes," so a link has no destination.
Tabs that only change what's shown on the current page are buttons with ARIA roles. The full ARIA tab pattern looks like this: put role="tablist" on the container, role="tab" on each button, and role="tabpanel" on each content section. Add aria-selected="true" to the active tab and aria-selected="false" to the others. A screen reader then announces something like "Tab 2 of 3, Reviews, selected" when the user navigates through.
Each panel has exactly one tab. Mixing links and buttons inside the same tablist would confuse both users and screen readers — they expect every tab in a set to behave the same way.
No better built-in element exists for on-page tab switching than buttons with ARIA roles. Some UI libraries ship a custom tab component, but under the hood those are just buttons with the same ARIA roles applied.
The scenario says "no URL changes" directly, which pins the answer down. With no URL to link to, it has to be button.

A top-level nav item "Products" that both navigates to /products AND expands a dropdown submenu when clicked.

A plain link can't toggle a submenu without JavaScript that overrides the link's normal click behavior. You'd end up breaking either the navigation or the toggle — and right-clicking "Open in new tab" would probably misbehave too.
A plain button has no URL destination. Right-click "Open in new tab" wouldn't work because there's nothing to open. The user also can't bookmark or share the /products page straight from the button.
A single HTML element can't do two different jobs at once — you can't be both a destination and a toggle. Split them into two controls sitting side by side: the "Products" text as an <a href="/products">, and a separate small chevron <button> next to it for expanding the submenu. Give the chevron button aria-expanded="false" (set it to "true" in JavaScript when the submenu is open) so screen readers announce its state. GOV.UK and the ARIA Authoring Practices Guide both recommend this split.
No single HTML element fits "navigate and toggle" at the same time. The answer really is both a link and a button — the only question is how to arrange them.
The two jobs — navigating and toggling — are always two separate things. No layout or styling context collapses them back into one element.

A video player's progress bar that lets the user click or drag to scrub to a specific timestamp.

Nothing to link to — the progress bar doesn't navigate to a URL. And there's no discrete list of timestamps you could enumerate as separate links.
A button is for a single discrete action: one click, one thing happens. Scrubbing is continuous along a range — the user picks any position between the start and the end of the video. A button can't express "pick a value along this range."
Neither link nor button fits individually, so combining them doesn't help either.
This is a slider. Use <input type="range"> — that's the built-in HTML element for picking a numeric value along a continuous range. The browser handles the visual track, the thumb, and the drag behavior for you. If you really can't use <input type="range"> (maybe the visual design can't be achieved with CSS alone), fall back to <div role="slider"> and add four ARIA attributes: aria-valuemin (usually 0), aria-valuemax (usually the total seconds), aria-valuenow (the current position), and — most importantly — aria-valuetext="2 minutes 34 seconds". Raw numbers in aria-valuenow don't mean anything useful to a screen reader user; aria-valuetext is the human-readable version that actually gets announced out loud.
Scrubbing is always a continuous range control regardless of the player's layout or styling. The answer doesn't depend on context.

A language selector in the footer showing EN, FR, and DE.

Right if each language corresponds to a real URL — like /fr or /de — and clicking changes the page to that URL. The user can then share /fr with a friend who reads French, or bookmark it to come back to the French version later.
Right only if the language switch is purely client-side with no URL change — the page stays at the same URL but the text re-renders in another language. Many single-page applications do this without URL support, which means users can't share or bookmark a specific language. A common mistake.
One control per language is what the interface calls for. A link and a button for each language would just clutter the footer.
A <select> with an <option> for each language is another solid choice, especially when you support many languages and listing them all as buttons or links would take too much space. Screen readers handle <select> natively.
The right element depends on how language switching is implemented. Per-language URLs like /fr → links (so the user can share and bookmark them). Client-side state with no URL change → buttons (or a <select>). Many SPAs choose the second route without ever adding URL support, which makes localized versions unshareable — worth checking before you pick.

A "Delete account" option at the bottom of a settings page.

Same problem as sign-out: a link pointing at /delete-account is a CSRF (Cross-Site Request Forgery) vulnerability. Any malicious site could include <img src="https://yoursite.com/delete-account"> and silently delete your user's account when they visit. Destructive actions must never be triggered by a simple URL request (a GET request).
A destructive action should send a DELETE request (the HTTP method whose job is removing a resource) or a POST request. Put the button inside a <form method="post">, or trigger the request from JavaScript. The typical flow: user clicks the button → a confirmation dialog appears asking "Are you sure?" → user confirms → the DELETE request goes out. Keep the confirmation in a dialog on the same page; sending the user to a separate /confirm-delete page just to ask "are you sure?" is unnecessary friction.
Only one delete control. Duplicating a destructive action is exactly how accidents happen — two buttons double the odds of a wrong click.
No other built-in HTML element fits "trigger a destructive action after confirmation" better than a button inside a form. <input type="submit"> would work too, but <button> is more flexible for styling and for including icons.
The security rule — don't expose destructive actions as GET-addressable URLs — is the same regardless of what the rest of the settings page looks like.

A filter chip "Shoes" that narrows search results and updates the URL to ?category=shoes.

The filtered state is part of the URL, which makes it shareable, bookmarkable, and survives a page reload. Right-clicking "Open in new tab" should give the same filtered view — links handle that for free. This is the same pattern as pagination: each filter state is a real URL the user can return to. Many e-commerce sites build filter chips with JavaScript that don't update the URL, which breaks the back button and makes filtered views impossible to share.
A button would run JavaScript that changes what's on screen but leaves the URL alone. The scenario explicitly says the URL updates, so a plain button isn't the right tool — the filter state needs to be part of the URL.
One control per filter. Showing a link and a button for each chip would just clutter the page.
A styled link is the simplest fit. If the design called for picking several filters at once with a dropdown feel, a <select multiple> or a group of <input type="checkbox"> could fit — but each individual chip is still a clickable thing that changes the URL, which is a link.
The scenario pins down that the URL updates, which removes the ambiguity. URL change → link.

A "Copy link" option that copies the current URL to the clipboard.

Nothing to navigate to — copying a URL doesn't take the user anywhere. A link without a destination doesn't make sense here.
A pure action — no navigation, no persistent state. The browser's navigator.clipboard.writeText(url) does the actual copying; a <button> triggers it. A common pattern: temporarily change the label to "Copied!" for a second or two after the copy succeeds, then revert. That's still a button, just with a bit of state for feedback.
One control to copy is all the user needs. A second copy button next to the first would just add clutter.
No other built-in HTML element fits this more naturally than a button. An <input type="text" readonly> showing the URL lets users select and copy manually, but that's a fallback for situations where JavaScript can't run — not a replacement for a proper Copy button.
Copying to the clipboard is the same kind of action everywhere. No surrounding context flips this to a link or another element.

The last item in a breadcrumb trail (Home › Products › Laptops), representing the page the user is currently viewing.

Make it a link to its own URL (<a href="/products/laptops" aria-current="page">Laptops</a>). A link lets users right-click "Copy link address," drag the item to the bookmarks bar, or share the URL — all things they genuinely do with breadcrumbs. aria-current="page" is a WAI-ARIA attribute (ARIA adds information for screen readers) that tells assistive tech "this is the current page," so screen readers announce it correctly even though it's still a link. Many sites visually style the current item without an underline so sighted users don't mistake it for something they should click.
A button implies there's an action to perform, but there's nothing to do — the user is already on this page. No click behavior makes sense for a button here.
One item in the breadcrumb trail represents the current page. A link plus a button for the same item would just confuse the user.
A <span> with aria-current="page" is the stricter pattern some sites use. Screen readers announce it correctly, and sighted users can't click it by mistake. The downside: users can't right-click to copy the URL, drag it to bookmarks, or share it — all real behaviors people use on breadcrumbs. The link version wins on utility.
Either the link version or the non-interactive <span> version is defensible. Neither changes because of surrounding context — the choice is about which utility tradeoff (bookmarkable vs. unclickable) matters more to your users.

A "Skip to main content" option at the very top of the page, visually hidden at rest but shown when it receives keyboard focus.

<a href="#main">Skip to main content</a> is the standard pattern. It navigates to a page fragment — the <main> element, or whatever element has id="main" — using the browser's native fragment behavior. No JavaScript needed. This is a critical accessibility feature for keyboard users: without it, every page load forces them to Tab through every nav link before they can reach the content. Requirements: make it the very first focusable element in the DOM, and use CSS to hide it visually at rest but reveal it when it receives :focus so keyboard users can see it as they Tab to it.
A button would need JavaScript to scroll the page or move focus. That's more code to maintain and it breaks if scripts fail to load. A link with a fragment does the same job natively — built into the browser, works everywhere.
One skip link is the established convention. Duplicating it just confuses keyboard users who learn to expect exactly one at the top of the page.
No other built-in HTML element does fragment navigation this cleanly. A well-styled <a> with href pointing at #main is the entire solution.
The skip-link pattern is the same on every page of every site. No surrounding context changes the answer.

A notification bell icon in the top nav that, when clicked, opens a dropdown showing recent notifications.

No URL change — clicking the bell just reveals a dropdown on the current page. The URL stays the same, the page doesn't navigate, so a link has no destination to point at.
Clicking the bell triggers a UI change (showing or hiding a dropdown), which is exactly a button's job. Two attributes matter here: aria-expanded="false" when the dropdown is closed and aria-expanded="true" when it's open, so screen readers announce the current state. And aria-label="Notifications" — because the button's only visible content is an icon, without an aria-label a screen reader would just announce "button" with no idea what it's for. A common beginner mistake: forgetting aria-expanded, which leaves screen reader users with no way to tell whether they've opened the dropdown.
One trigger per dropdown. A second control that opens the same dropdown would just add noise without adding capability.
No other built-in HTML element fits "reveal a dropdown on click" better than a button. The newer Popover API (adding popovertarget to a button and popover to a <div>) is a modern way to wire them together without any JavaScript, but you still need a button as the trigger.
A trigger for an in-page dropdown is always a button, regardless of the icon, the position in the nav, or the surrounding styling. No context flips this to a link.