EduFlow

Section 2 · Semantic HTML First

Buttons, Links and the Clickable Div

17 min3 min readCode playground

Lesson at a glance

Choose correctly between a link and a button, and replace clickable divs and href=“#” links with native elements that work for keyboard and screen reader users.

  1. 1Links go, buttons do
  2. 2What the clickable div is missing
  3. 3Buttons in forms, and disabled buttons
  4. 4Text that makes sense alone

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Ticketly’s “Add to basket” control looks like a button: rounded corners, a brand colour, a pointer cursor on hover. It is a div with an onclick. Click it with a mouse and it works perfectly, which is exactly why nobody on the original team noticed. Press Tab and focus jumps straight past it. Samir’s screen reader reads it as plain text. A switch user can’t reach it at all.

Links go, buttons do

The decision between the two native controls comes down to one question: does it take the user somewhere?

  • A link (<a href="…">) navigates: to another page, another part of this page (#lineup), a file, an email address. Screen readers announce it as “link”. Users expect to open it in a new tab, copy its address, see it in history. It activates with Enter.
  • A button (<button>) performs an action on the current page: add to basket, open a dialog, submit a form, toggle a filter. Screen readers announce it as “button”. It activates with Enter and Space.
Decision flow: if activating the control changes the URL or moves to another place, use an a element with href; if it changes something on the current page, use a button element; never a div or span with a click handler. Does it take the user to a new URL or place? yes no, it acts here <a href="…"> <button> Never: div or span with onclick
Picking the right element

Styling is irrelevant to the choice. A link can look like a big call-to-action and a button can look like a text link; CSS handles appearance, the element handles behaviour.

What the clickable div is missing

To understand why the native element wins, count what a div lacks. Here is the minimum to rebuild a button by hand:

HTML
<div class="btn" role="button" tabindex="0">Add to basket</div>
JavaScript
const fake = document.querySelector('.btn');
fake.addEventListener('click', addToBasket);
fake.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') addToBasket();
  if (event.key === ' ') event.preventDefault(); // stop the page scrolling
});
fake.addEventListener('keyup', (event) => {
  if (event.key === ' ') addToBasket(); // native buttons fire on Space release
});

That is a role, focus, Enter, Space and scroll prevention, and it still lacks a disabled state, form submission, and the way Windows contrast themes (forced colors mode) style buttons. Compare:

HTML
<button type="button" class="btn" onclick="addToBasket()">Add to basket</button>

One element, and every behaviour is correct in every browser and every screen reader. This is the pattern you will see throughout the course: native HTML first, because the browser has already done the hard work.

Buttons in forms, and disabled buttons

Inside a <form>, a <button> defaults to type="submit". Ticketly’s checkout has an “Apply promo code” button that kept submitting the whole form. The fix is one attribute:

HTML
<button type="button" onclick="applyPromo()">Apply promo code</button>

The disabled attribute removes a button from the tab order, so keyboard users tabbing through the form skip straight past it, and nobody is told why it is unavailable. A screen reader in browse mode still finds it, announced as “unavailable”, but with no reason attached. For a “Pay now” button that is waiting on a valid card, consider leaving it enabled and explaining the problem when it is pressed, which you will build in Accessible Forms.

Text that makes sense alone

Screen reader users often pull up a list of all links or buttons on a page, and voice control users say the visible text to click it. “Read more”, “Click here” and “Details” are meaningless in that list. WCAG 2.4.4 asks that a link’s purpose be clear from its text, or its text plus its surrounding sentence or list item. Aim higher than the minimum: write “Friday line-up and set times”, not “Read more”. If the design insists on short visible text, you will learn in Accessible Names and Descriptions how to extend the name without breaking voice control.

One more link habit: if a link opens a new tab or downloads a file, say so in its text, for example “Venue guide (PDF, 2 MB)” or “Seating plan (opens in a new tab)”. An unexpected new tab breaks the Back button for screen reader and magnifier users, who often don’t notice that the window changed and wonder why they can’t return.

Real buttons and links fix the controls that act. Next come the controls that collect information, and the place where most inaccessible sites lose their customers: forms.

Key takeaways

  • A link takes the user somewhere (a new URL or a fragment); a button does something on the current page.
  • Native buttons are focusable, announced as buttons, and activate with both Enter and Space, all for free.
  • A div with a click handler has no role, no focus and no keyboard support; rebuilding those by hand takes four separate fixes and is easy to get wrong.
  • Inside a form, give non-submitting buttons type=“button”, and write link and button text that makes sense out of context.

Further reading

Check your understanding

4 questions

Question 1 of 4

Which of these should be a link rather than a button?

Question 2 of 4

A developer “fixes” this fake button. What is still missing?

HTML
<div role="button" tabindex="0" onclick="addToBasket()">Add to basket</div>
Question 3 of 4

Inside Ticketly’s checkout form, the “Apply promo code” button submits the whole form and shows “Card number required”. Why?

Question 4 of 4

A screen reader user pulls up a list of links on Ticketly and hears “Read more, Read more, Read more”. What is the best fix?

Practice

Code playground

Replace Ticketly's fake controls

The Friday pass card has three controls, and none of them is the right element:

  • Add to basket is a div with onclick. It runs addToBasket().
  • Remove one is an <a href="#"> that runs removeFromBasket().
  • See the Friday line-up is a span that changes the URL hash to #lineup.

Rebuild each with the correct native element, keeping the visible text exactly as it is. The two actions must still update the basket count. Keep the class names if you like, so the styling stays.

<p>Basket: <span id="basket-count">0</span> tickets</p>
<article class="pass">
  <h2>Friday pass</h2>
  <p>SAR 180, standing.</p>
  <div class="btn" onclick="addToBasket()">Add to basket</div>
  <a href="#" class="text-btn" onclick="removeFromBasket(); return false;">Remove one</a>
  <span class="link" onclick="location.hash = '#lineup'">See the Friday line-up</span>
</article>
<h2 id="lineup">Friday line-up</h2>
<p>Nour Quartet, The Saltmarsh Band.</p>Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • “Add to basket” is a real button
  • Clicking “Add to basket” adds one ticket to the basket count
  • “Remove one” is a real button that lowers the count
  • “See the Friday line-up” is a link with href ending in #lineup
  • No div or span click handlers, and no links with href=“#” or no href, remain