---
title: 'Link and Button Distinction'
source: 'https://academia.sh/en/courses/accessible-patterns/link-and-button-distinction'
course: 'Accessible Component Patterns'
language: en
updated: '2026-08-19T05:19:51+00:00'
license: 'CC BY-SA 4.0'
---

# Link and Button Distinction

The decision rule based on source change, comparing the two components' keyboard contracts, counting the losses a mismatch produces, and auditing link text against purpose criteria.

Every control in the borrowing form ran an action. The record titles in the results
table, by contrast, do a different job: they take the user to the record detail page.
The two can be made to look alike enough in the presentation layer to substitute for one
another — a link can be drawn boxed and filled, a button can look like underlined text.

In the tree, though, the two are distinct, and this distinction is not merely a naming
preference: the two components' keyboard contracts, the extra capabilities the browser
offers, and what the user expects from them differ. This lesson ties the distinction to
a decision rule and counts what its violation costs.

## The Decision Rule

The rule is a single sentence: **if the user is going to another resource, it is a link;
if not, it is a button.** The measure is whether the address bar's address changes after
it runs; a download also counts as accessing a resource.

Some cases the rule works through look counterintuitive. A control that writes filter
selections to the address — an arrangement that turns the "Only items on the shelf"
state into a shareable address — is a link, because what it produces is an address that
can be sent to someone else. A control that jumps to a section within the page is also a
link; its target is a document fragment, and it enters browser history. A filter
application that refreshes the results table but does not change the address, by
contrast, is a button.

## The Native Element First

The link's counterpart is the `a` element carrying the `href` attribute; the button's
counterpart is the `button` element. An `a` element without `href` is a meaningless node
in the tree: it produces no role, takes no focus, and no key operates it.

The link brings four capabilities the button does not provide: copying the address,
opening in a new tab from the context menu, opening with the middle click, and appearing
in the link list assistive technology produces. None of these are things that can be
written in code; they are behaviors the browser ties to the link element.

## Role, Name, and State

Both components' names are computed from their content. The link carries an additional
constraint: the name has to say **where it goes**.

Having a link that reads "Detail" at the end of every row is a common arrangement in a
results table. In a forty-row table, this produces forty identically named links in the
tree. A user who opens the link list sees forty identical rows and cannot tell where any
of them go. The fix is either to expand the link text to the record's name or to
complete the visible text with a hidden segment so the name becomes "The Language of
Structures detail."

The link has no persistent state declaration. A link showing the current page carries
`aria-current="page"`; this is not a state, it is a declaration of position, and it is
taken up in navigation components.

## Two Keyboard Contracts

| Key / action | Link | Button |
|---|---|---|
| `Tab` | A tab stop. | A tab stop. |
| `Enter` | Follows the link. | Runs the button. |
| `Space` | Scrolls the page down. | Runs the button. |
| Context menu / middle click | Opens in a new tab, copies the address. | Has no counterpart. |

The difference in the Space key is the mismatch's most visible consequence. A user who
presses Space on an action control built like a link scrolls the page; on a navigation
control built like a button, the "open in new tab" option never appears in the context
menu.

## Auditing the Mapping

```js
// link-or-button.mjs — formalizing and auditing the decision between a link and a button

// Decision rule: if the source changes (address or download), link; otherwise button.
const decide = (n) => (n.addressChanges || n.download ? "link" : "button");

// Behavior produced by the markup
function produced(m) {
  const isLink = m.element === "a" && m.href !== undefined;
  const isButton = m.element === "button";
  const role = m.role ?? (isLink ? "link" : isButton ? "button" : "generic");
  return {
    role,
    focusable: isLink || isButton || m.tabindex === 0,
    enter: isLink || isButton || m.keysWork === true,
    space: isButton || m.keysWork === true,
    newTab: isLink,          // context menu and middle-click only work when there is an address
    addressCopies: isLink,
    inLinkList: role === "link",
  };
}

// --- Controls in the catalog interface ---------------------------------------
const EXAMPLES = [
  ['<a href="/record/K-118">The Language of Structures</a>',
   { addressChanges: true }, { element: "a", href: "/record/K-118" }],
  ['<a href="#" tabindex="0">Clear filters</a>',
   { addressChanges: false }, { element: "a", href: "#", keysWork: true }],
  ['<button>Record detail</button> (navigates via code)',
   { addressChanges: true }, { element: "button" }],
  ['<a href="/borrow/K-118" role="button">Borrow</a>',
   { addressChanges: true }, { element: "a", href: "/borrow/K-118", role: "button" }],
  ['<div tabindex="0">Search</div>',
   { addressChanges: false }, { element: "div", tabindex: 0 }],
  ['<button type="submit">Search</button>',
   { addressChanges: false }, { element: "button" }],
  ['<a href="/citation/K-118.pdf" download>Download citation</a>',
   { addressChanges: false, download: true }, { element: "a", href: "/citation/K-118.pdf" }],
  ['<span role="link" tabindex="0">Help</span>',
   { addressChanges: true }, { element: "span", role: "link", tabindex: 0 }],
];

const yn = (b) => (b ? "yes" : "no");

console.log("markup".padEnd(62) + "required".padEnd(10) + "role".padEnd(10) +
  "Enter".padEnd(7) + "Space".padEnd(8) + "new tab".padEnd(12) + "link list");
for (const [markup, intent, m] of EXAMPLES) {
  const u = produced(m);
  console.log(markup.padEnd(62) + decide(intent).padEnd(10) + u.role.padEnd(10) +
    yn(u.enter).padEnd(7) + yn(u.space).padEnd(8) + yn(u.newTab).padEnd(12) +
    yn(u.inLinkList));
}

console.log("\nrule check:");
let findings = 0;
for (const [markup, intent, m] of EXAMPLES) {
  const g = decide(intent);
  const u = produced(m);
  const b = [];
  if (g === "link" && u.role !== "link")
    b.push("navigation declared as a button: address cannot be copied, cannot open in a new tab");
  if (g === "button" && u.role === "link")
    b.push("action declared as a link: enters history, target address is meaningless");
  if (!u.focusable) b.push("cannot be focused with the keyboard");
  if (!u.enter) b.push("Enter key does not work");
  if (u.role === "button" && !u.space) b.push("button role declared, Space key does not work");
  if (u.role === "link" && m.element !== "a") b.push("link role declared, no address");
  for (const x of b) { findings++; console.log("  " + markup.padEnd(62) + x); }
}
console.log(`\n${EXAMPLES.length} examples, ${findings} findings`);

// --- Decision table -----------------------------------------------------------
console.log("\ndecision table:");
console.log("behavior".padEnd(52) + "component");
const BEHAVIORS = [
  ["goes to the record detail page", { addressChanges: true }],
  ["filters the results table, address does not change", { addressChanges: false }],
  ["writes the filter state to the address", { addressChanges: true }],
  ["downloads the citation file", { addressChanges: false, download: true }],
  ["submits the borrow request", { addressChanges: false }],
  ["jumps to a section within the page", { addressChanges: true }],
];
for (const [behavior, n] of BEHAVIORS) console.log(behavior.padEnd(52) + decide(n));
```

```
markup                                                        required  role      Enter  Space   new tab     link list
<a href="/record/K-118">The Language of Structures</a>        link      link      yes    no      yes         yes
<a href="#" tabindex="0">Clear filters</a>                    button    link      yes    yes     yes         yes
<button>Record detail</button> (navigates via code)           link      button    yes    yes     no          no
<a href="/borrow/K-118" role="button">Borrow</a>              link      button    yes    no      yes         no
<div tabindex="0">Search</div>                                button    generic   no     no      no          no
<button type="submit">Search</button>                         button    button    yes    yes     no          no
<a href="/citation/K-118.pdf" download>Download citation</a>  link      link      yes    no      yes         yes
<span role="link" tabindex="0">Help</span>                    link      link      no     no      no          yes

rule check:
  <a href="#" tabindex="0">Clear filters</a>                    action declared as a link: enters history, target address is meaningless
  <button>Record detail</button> (navigates via code)           navigation declared as a button: address cannot be copied, cannot open in a new tab
  <a href="/borrow/K-118" role="button">Borrow</a>              navigation declared as a button: address cannot be copied, cannot open in a new tab
  <a href="/borrow/K-118" role="button">Borrow</a>              button role declared, Space key does not work
  <div tabindex="0">Search</div>                                Enter key does not work
  <span role="link" tabindex="0">Help</span>                    Enter key does not work
  <span role="link" tabindex="0">Help</span>                    link role declared, no address

8 examples, 7 findings

decision table:
behavior                                            component
goes to the record detail page                      link
filters the results table, address does not change  button
writes the filter state to the address              link
downloads the citation file                         link
submits the borrow request                          button
jumps to a section within the page                  link
```

## Reading the Findings

**A link with an empty address is wrong twice over.** The filter-clearing control in the
second row is an action but has been declared as a link. It has two consequences: a
meaningless entry enters browser history, and nothing happens when the user presses the
back button; a row with no destination appears in the link list. And if the user opens
it in a new tab from the context menu, it brings up a blank page.

**A role declaration does not make a link a button.** The fourth row shows this. When
`role="button"` is written, the role in the tree changes, but the element is still a
link: the Space key does not work, and the ability to open in a new tab remains. What
results is a component that is a button on screen, a button in the tree, and a link in
behavior, and the three contradict one another.

**A role declaration does not add behavior either.** The `span` element carrying
`role="link"` in the last row is focusable, because `tabindex` has been written; but it
does not respond to the Enter key and carries no address to go to. It appears in the
link list, and selecting it goes nowhere.

**Navigation done with a button produces a silent loss.** In the third row, no defect is
visible on screen: the button takes focus, works with the Enter and Space keys, and the
page opens. What is lost is the four capabilities the link brings. The user cannot open
the record detail in a new tab, cannot copy its address, and cannot find it in the link
list.

## Measurable Constraints

**2.4.4 Link Purpose (In Context)** requires a link's purpose to be understood from its
own text or the context it sits in; **2.4.9 Link Purpose (Link Only)** requires the same
thing from the text alone and is AAA level. The "Detail" links in the table satisfy the
first thanks to the table's context, but not the second.

**1.4.1 Use of Color** requires links within text not to be distinguished from the
surrounding text by color alone. Underlining satisfies this condition directly. If color
distinction alone is to count, two conditions are required together: the link color has
to carry at least a 3:1 contrast ratio with the surrounding text, and a second visual
distinction has to appear on focus or hover.

**2.5.8 Target Size** excludes links within text through the inline exception; a link in
the middle of a paragraph has a height tied to the line height, and enlarging it would
break the text. Links in the results table, by contrast, are not inline and fall within
the criterion.

**3.2.5 Change on Request** ties opening a new window to the user's request. If a link
opens in a new tab, this behavior has to be declared as part of its name; a small icon
that appears on screen declares nothing if it has no alternative text.

## Common Mistake

**The addressless link.** Catching it needs no tooling: every `a` element that does not
carry `href`, or whose value is `#`, is a finding. Automated checkers' presence rule
catches this.

**Many links with the same name.** Catching it is a uniqueness check: the link names on
the page are collected, and pairs that share a name but go to different addresses are
listed.

**Appearance determining the decision.** Building a link for the reason "this should
look like a button," or the reverse. Catching it is the decision table: what the
component does is asked, not what it looks like. Visual weight can be given to either
component independently.

## Summary

- The decision rule is a single sentence: if the user is going to another resource, it
  is a link; if not, a button; a download also counts as accessing a resource.
- A link brings the capabilities of copying the address, opening in a new tab, opening
  with the middle click, and appearing in the link list; these cannot be written in
  code.
- The Enter key works on both; the Space key only runs the button, and on a link it
  scrolls the page.
- A role declaration neither changes nor adds behavior; a link carrying `role="button"`
  produces a component that contradicts itself on screen, in the tree, and in behavior.
- A link's name has to say where it goes; the repeated "Detail" texts in the table do
  not satisfy the 2.4.9 criterion.
- Links within text fall under the inline exception of the 2.5.8 target size criterion;
  links in a table row do not.

## Next Step

This topic specified controls one at a time; each one's boundary was its own box. In the
results table's card view, though, the boundary blurs: a card carries an image, a title,
an author, a year, and two controls, and the designer wants the card's **entirety** to be
clickable. That request requires putting a second control inside a single link, and the
pattern breaks here. The next lesson specifies the card: how content grouping is
declared, the three solutions to the clickability problem, and what each one does to the
tab stop count.
