You’ve got a component to wire up and a design that looks like… a rectangle with text in it. Classic. Do you reach for <button>
or <a>
? For a mouse user, either might “work.” For a keyboard user or someone on a screen reader, the wrong choice is a booby trap. Picture a user pressing Space on something that looks like a button and nothing happens, or hearing “Link” and suddenly a file gets deleted. That gap between what an element promises and what it does is where trust dies. By the time you ship this, you should be able to look at any interactive element and decide between buttons vs links without second-guessing.
The Rule Behind Buttons vs Links
There’s one rule worth tattooing on your coding hand:
Links go places. Buttons do things.
If an interaction navigates to a new location (a different route, an external page, a same-page anchor, or a file), it’s a link. If it performs an action on the current page (submits, toggles, opens, closes, mutates UI, updates data), it’s a button. Keep that mental model tight and the rest—semantics, keyboard behavior, screen reader expectations—snaps into alignment.
Let’s make that concrete. Screen readers announce name and role. “Link” tells the user to expect navigation; “Button” says “an action is about to happen.” Keyboard support follows suit: links activate with Enter; buttons activate with Enter and Space. That difference isn’t trivia—it’s the platform contract.
Why This Tiny Decision Matters More Than It Looks
First, semantics affects product quality. When the role and behavior match, users trust your interface—especially users who navigate by role (“jump me to the next button”). Second, robustness. A real link with a real href
will still navigate if JavaScript takes a day off. A real button with type="submit"
will submit the form without your custom event handlers. Getting buttons vs links right means your UI remains reliable under bad Wi-Fi, flaky extensions, or partial loads.
And yes, standards. Mislabeling roles or breaking keyboard activation is how you wander into WCAG failures like 2.1.1 (Keyboard) or 4.1.2 (Name, Role, Value). It’s not about box-ticking; it’s about delivering predictable, testable behavior.
When It Should Be a Button
If the thing does something on this page, start with <button>. That covers submitting a form, opening a modal, toggling a menu, expanding an accordion, saving a record, deleting an item, starting a fetch, pausing media—anything that changes state without changing URL.
Here’s what not to do:
<!-- Wrong: Looks like a link, acts like a button, breaks expectations -->
<a href="#" onclick="submitForm()">Submit</a>
This announces as “link,” ignores the Space key unless you recreate that yourself, and fails if JavaScript is blocked. Use the element that already does the job:
<!-- Right: Predictable, robust, testable -->
<button type="submit">Submit</button>
Toggles are another frequent offender. If you’re opening and closing a menu, use a button and reflect state:
<button
type="button"
aria-expanded="false"
aria-controls="mainmenu"
id="menubutton">
Menu
</button>
<nav id="mainmenu" hidden>…</nav>
When you flip the menu, flip aria-expanded
and manage the hidden
attribute. Users get the correct announcement, and you get keyboard support for free.
A quick style note while we’re here: don’t remove the focus outline unless you replace it with something obvious. Minimalism is lovely; invisible focus isn’t.
button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
When It Should Be a Link
If the action navigates—new page, external site, file download, or a jump within the same page—reach for <a>
with a real href
. That gives you built-in affordances like “open in new tab,” a visible status bar URL, and the user’s browser history doing its thing.
Bad pattern:
<!-- Wrong: Navigation disguised as a button -->
<button onclick="location='/about'">Learn more</button>
Fix it:
<!-- Right: It’s navigation, so it’s a link -->
<a href="/about">Learn more</a>
A few patterns that absolutely belong to links:
- Skip links: let keyboard users bypass repeating navigation.
<a href="#main" class="skip-link">Skip to main content</a>
<main id="main">…</main>
- Downloads: if you can link it, link it.
<a href="/report.pdf" download>Download report (PDF)</a>
- Breadcrumbs and primary nav: always links. Users expect middle-clicks and new tabs to work.
Give the link text meaning. “Click here” ages badly outside its paragraph. “Download Q3 report (PDF)” holds up wherever a user encounters it—like a screen reader’s Links List.
Styling Without Lying: Buttons vs Links in Disguise
Design will ask you to make links look like buttons or vice-versa. That’s fine visually, as long as the semantics don’t lie. If it navigates, keep it an <a>
—then style it like a button. If it acts, keep it a <button>
—then style it to match your theme. Users might not see the markup, but assistive tech will, and so will your QA.
A few constraints worth honoring while you polish:
- Don’t rely on color alone to signal “this is a link.” Underlines or a small icon are your friends.
- Keep contrast sane (think 4.5:1 for normal text). Your future self, reading in sunlight, says thanks.
- Targets should be comfortably tappable—around 44×44 CSS pixels is a good baseline.
- Maintain a visible focus style. If you nuke it, put a better one back.
Testing the Decision: Buttons vs Links Under Real Inputs
Automated tools are a fast smoke test—Lighthouse or WAVE will happily rat out missing names, broken contrast, or off-screen focus. Treat them as lint for accessibility. They won’t catch intent.
Intent is a manual game:
- Screen reader pass: with VoiceOver (macOS/iOS), NVDA or JAWS (Windows), confirm elements announce the correct role and name: “Button, Submit,” not “Link, Submit.”
- Keyboard pass: tab through. Links should activate with Enter; buttons with Enter and Space. Watch focus—no disappearing acts.
- Voice control: say “Click Submit” or “Go to Pricing,” and make sure the right control responds by name.
If any of those feel off, they are off. Fix the semantics first; the bugs usually evaporate.
Real-World Fixes That Come Up in Code Reviews
Most bugs we see with buttons vs links boil down to “we styled the wrong element” or “we tried to be clever with roles.” Two quick refactors cover a lot of ground:
Submitting with an anchor:
<!-- Wrong -->
<a href="#" onclick="submit()">Submit</a>
<!-- Right -->
<button type="submit">Submit</button>
Menu toggles built on anchors:
<!-- Wrong -->
<a href="#" onclick="openMenu()">Menu</a>
<!-- Right -->
<button type="button" aria-expanded="false" aria-controls="mainmenu">Menu</button>
If you truly, absolutely must build a custom control—and you’ve checked that <button>
won’t cut it—own the keyboard and state explicitly:
<!--Only if native won’t work -->
<div role="button" tabindex="0" aria-pressed="false" id="play">Play</div>
<script>
const el = document.getElementById('play');
el.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); el.click(); }
});
el.addEventListener('click', () => {
const next = el.getAttribute('aria-pressed') !== 'true';
el.setAttribute('aria-pressed', String(next));
});
</script>
That’s a lot of ceremony to re-implement what <button>
just… does. Treat this as a last resort.
A Short Checklist (Pin It Next to Your Linter)
- Navigation?
<a href="…">
. Action?<button>
. - Make the accessible name obvious: visible text or a clear
aria-label
. - Preserve expected keyboard behavior (Enter vs Enter+Space).
- Keep focus visible and movement logical.
- Prefer native elements; avoid role-swapping unless there’s no alternative.
Wrapping It Up
Interfaces are a web of tiny decisions, and this is one of the tiniest. But consistent, honest semantics make everything else easier: testing, maintenance, onboarding new devs, and—most importantly—using the thing. Treat buttons vs links as a contract. Say what the element is, and make sure it behaves that way across mouse, keyboard, screen readers, and voice.
If you want a quick heuristic while you code: would a middle-click make sense here? If yes, it’s probably a link. Would “Click Submit” make sense to a voice user? If yes, it’s probably a button. Keep those instincts sharp and your UI will feel clean, resilient, and respectful by default.
If your team wants a second set of eyes on patterns like these—or you need help pressure-testing components before they scale—216digital is happy to jump in. Schedule an ADA briefing and we’ll help you turn the “tiny” choices into durable, inclusive defaults.