ESC

Type to search the knowledge base.

Links vs Buttons

Navigate with links, act with buttons — correct semantics, keyboard behavior, and the SPA footguns that break both.

beginner4 min read
  • html
  • links
  • buttons
  • accessibility
  • semantics

Links go somewhere. Buttons do something. That split drives semantics, keyboard expectations, middle-click/open-in-new-tab behavior, and SEO. Using the wrong element is one of the most common frontend accessibility bugs — and one of the easiest to fix.

Docs: MDN <a>, MDN <button>, APG, HTML spec.

The rule of thumb

User intent Element Why
Go to a URL / route <a href="…"> Real navigation, shareable URL, open in new tab, crawlable when applicable
Submit a form, toggle UI, delete, play, open dialog <button type="…"> Action without changing location (or not primarily navigation)
“Looks like a button, navigates” Still <a> — style it Keep navigation semantics
“Looks like a link, runs JS only” Usually <button> Fake links break expectations

If the primary result is a new location (including in-app routes that update the URL), start with a link. If the primary result is an action on this page, start with a button.

What browsers and AT give you for free

<a href="/pricing">Pricing</a>
<a href="https://example.com/docs">Docs</a>
  • Enter and (typically) activation conventions users know
  • Middle-click / Cmd-click → new tab
  • “Copy link address,” share sheet, status-bar URL preview
  • In lists of navigation, announced as link
  • Without href, an <a> is not a full link — don’t use bare <a> as a button

Button (<button>)

<button type="button" onclick="save()">Save</button>
<button type="submit">Create account</button>
  • Space and Enter activate (links are primarily Enter; Space scrolls the page for focused links in many browsers)
  • Form association with type="submit" / reset
  • Disabled state via disabled
  • Announced as button

Using <div role="button" tabindex="0"> means you reimplement keyboard, disabled semantics, and form behavior. Prefer native.

SPA routers

Client routers still benefit from real anchors:

// Prefer the router's Link that renders <a href="/about">
<Link to="/about">About</Link>

// Avoid
<span onClick={() => navigate('/about')}>About</span>

Reasons:

  1. Progressive enhancement / open-in-new-tab.
  2. Screen reader and browser features still see a link.
  3. Cmd-click and “open in new tab” work when href is real.
  4. Analytics and crawlers get URLs.

preventDefault on click for client navigation is fine on an actual href. Replacing the anchor with a clickable div is not.

When product design blurs the line

Button-styled navigation

<a class="btn btn--primary" href="/signup">Start free trial</a>

Correct. CSS does not change the element choice.

<button type="button" class="linkish" id="show-more">Show more</button>

Also correct. Don’t use <a href="#"> or <a href="javascript:void(0)"> for actions — that’s a legacy anti-pattern (history pollution, focus weirdness, wrong role).

Download

<a href="/report.pdf" download>Download report</a>

Navigation-ish resource fetch → link. A button that triggers a JS-generated blob download can stay a button if there’s no stable URL.

Same-page targets

<a href="#billing">Billing section</a>

In-page navigation → link. Manage focus to the target when needed for skip links / accessibility.

Icon-only controls

Every control needs an accessible name:

<a href="/settings" aria-label="Settings">
  <svg aria-hidden="true">…</svg>
</a>

<button type="button" aria-label="Close">
  <svg aria-hidden="true">…</svg>
</button>

Visible text is better when space allows. aria-hidden on decorative SVGs prevents double announcements. See accessible SVGs.

Disabled and “busy”

  • Prefer disabled on <button> for unavailable actions.
  • Links don’t have a native disabled state; use aria-disabled="true" plus prevent navigation, or render plain text if it’s not interactive. Styling a link gray without removing interactivity is a trap.
  • For in-flight submits, disable the button and expose busy state (aria-busy on a region, or live text “Saving…”).

Security and UX side notes

  • target="_blank" on links should include rel="noopener noreferrer" (modern browsers default noopener for _blank, but be explicit in older targets). Tell users when a new tab opens if it matters.
  • Never put untrusted HTML inside link/button content without escaping — classic XSS sink via markup or javascript: URLs on href.
  • Large hit targets help Core Web Vitals indirectly (fewer rage clicks) and meet touch-size guidance.

Decision flowchart (short)

  1. Does it change the URL / take you to a resource? → <a href>
  2. Does it submit a form? → <button type="submit">
  3. Otherwise an on-page action? → <button type="button">
  4. Must it look like the other one? → Style it. Don’t change the element.

Interview angle

State the navigate vs act rule. Explain Space vs Enter differences at a high level. Call out href="#" anti-pattern and SPA Link rendering real anchors. Mention accessible names for icon-only controls.

Live coding: toolbar with “Docs” (link), “Save” (button), and icon-only “Close” (button + aria-label).

Further reading

Related guides