The Div With a Click Handler: Why Semantic HTML Beats ARIA Every Time

A styled div can look exactly like a button and still be invisible to half the ways people interact with your app. Native elements do the heavy lifting — if you let them.

<div onClick>no focus · no Enter · no role<button>focus, keys, role — freeARIA is the last resort
Steve
Accessibility (A11Y) Specialist
Jan 7, 2026
6 min read

The pull request looked great. Clean styling, tidy state management, tests that actually tested things. Then I scrolled to the markup:

<div className="btn btn-primary" onClick={handleSave}>
  Save changes
</div>

Rendered in a browser, that div is pixel-for-pixel identical to a real button. And that's exactly what makes it dangerous: to a sighted mouse user it's a button, and to everyone else it's a paragraph that happens to have opinions about padding. You can't tab to it. Enter and Space do nothing. A screen reader announces it as plain text, if it announces it at all. Voice control users who say "click Save changes" get silence, because as far as the accessibility tree is concerned, there is no button named Save changes on this page.

I've stopped thinking of this as a markup style choice. It's a functionality bug that only affects people who aren't in the room when the demo happens.

Everything a real button does while you're not looking

Here's what you get, free, the moment you type <button>:

  • It's focusable — Tab reaches it, in the right order, with no tabindex bookkeeping.
  • Enter and Space both activate it, with the correct native quirks (Space fires on key up, and scrolling is suppressed while it's held — details nobody reimplements correctly).
  • Screen readers announce it as a button with its label: "Save changes, button." Users instantly know what it is and how to operate it.
  • Voice control software can target it by its accessible name.
  • It participates in forms, respects disabled, and shows up correctly in the browser's accessibility tree, high-contrast modes, and autofill heuristics.

That's a decade of browser engineering, and the fix costs one word:

// Before: invisible to keyboards, screen readers, and voice control
<div className="btn btn-primary" onClick={handleSave}>
  Save changes
</div>

// After: everything works, and it's less code
<button className="btn btn-primary" onClick={handleSave}>
  Save changes
</button>

If the objection is "buttons are hard to style," I have good news from the modern web: appearance: none plus your existing classes and you're done. There hasn't been a real styling reason to fake a button in years.

Button or link? Watch what happens next

The second question I ask in review isn't "is this a native element" but "is it the right native element." The test is behavioral, not visual: what happens when you activate it?

  • If it takes you somewhere — the URL changes, a new page or view loads — it's a link. Use <a href>.
  • If it does something here — submits, toggles, opens a dialog, deletes a row — it's a button.

Style either one however the design demands; a link is allowed to look like a big rounded button. But the underlying element sets real expectations. Screen reader users routinely pull up a list of all links on a page to scan for navigation — your button-shaped <a> will show up there, and your link-shaped <button> won't. Links support middle-click, "open in new tab," and "copy link address"; buttons don't and shouldn't. When you hear "link" announced, you expect to go somewhere. When the element lies about its nature, every one of those expectations breaks quietly.

ARIA is a promise you have to keep

Somewhere along the way, role and aria-* attributes picked up a reputation as accessibility seasoning — sprinkle some on and the dish is fixed. The truth is closer to the opposite. ARIA changes what assistive technology says about an element and changes nothing about what the element does.

Add role="button" to that div and a screen reader will now announce "Save changes, button" — and then Enter will do nothing, because ARIA made a promise your JavaScript hasn't kept. Honoring it by hand looks like this:

<div
  role="button"
  tabIndex={0}
  onClick={handleSave}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      handleSave();
    }
  }}
>
  Save changes
</div>

And even after all that, you've rebuilt maybe sixty percent of a button. No native disabled semantics, no form participation, none of the subtle activation behavior, and a standing invitation for the next refactor to break the keydown handler without anyone noticing.

This is why the W3C's own guidance opens with what's affectionately called the first rule of ARIA: don't use ARIA. If a native element exists with the semantics and behavior you need, use it. ARIA earns its keep in genuinely custom widgets — comboboxes, tree views, tab panels — where HTML has no native equivalent and you're consciously signing up to implement the full keyboard contract yourself. It's a power tool for the last mile, not a patch for skipping the first one.

The div arrives pre-written now

I'd be leaving out half the story if I didn't mention where these fake buttons increasingly come from. Code assistants are trained on the web, and the web's median markup is div soup — so div soup is what they confidently produce. Ask for "a card with a clickable action" and there's a decent chance you get a <div onClick>, sometimes with a decorative role attribute added like a garnish, which is arguably worse because it looks considered.

I don't say this to knock the tools — they're genuinely useful, and the markup is fixable in seconds once you see it. The point is that the responsibility has shifted. When markup was hand-written, the person typing it made the semantic choice. Now the choice often arrives pre-made and wrong, and the human in the loop is the reviewer. Reading a generated diff, "what element is this, really?" belongs on your mental checklist right next to "is this logic correct?"

A reviewer's shortlist

When UI markup crosses your desk — yours, a teammate's, or a model's — here's the quick pass I run:

  • onClick on a div or span? It's a button or a link wearing a costume. Ask which, and swap it.
  • Does activating it navigate? Then it's <a href>, even if it looks like a button. Otherwise <button>.
  • See a role attribute? Ask what native element was avoided, and whether the full keyboard behavior behind that role actually exists.
  • Tab to it and press Enter. Thirty seconds in a browser settles arguments that thirty minutes of diff-reading won't.
  • Default to the boring element. Boring is what browsers, screen readers, and future teammates have all agreed on.

The empowering part of all this is how little it asks of you. You don't need to memorize the ARIA spec or become an accessibility specialist to get this right. You mostly need to let HTML do its job — and to notice, in review, when something is working very hard to avoid doing so.

semantic-htmlariabuttonscode-review