Accessibility 5 min read

What you give up when you use a div instead of a button

A clickable div and a real button look identical. Here is every behavior the browser stops handing you, and what it costs to write back.

Illustration headed 'Web Accessibility — inclusive design, equal access for everyone'. Four people stand around a large laptop: a woman with a walking cane, a man in a wheelchair using a laptop, a man pointing at the screen, and a woman wearing dark glasses with a guide dog. Badges around them read Keyboard Friendly, Screen Reader Support, High Contrast, Closed Captions, Keyboard Navigation, and Readable Text.
<div class="btn" onclick="save()">Save</div>

That line passes design review. It has the padding and the hover state, the cursor turns into a pointer, and in a screenshot you can’t tell it from the real thing.

Tab to it and nothing happens. The browser never offers it focus, so the keyboard never reaches it, so the handler hanging off it may as well not exist for anyone who isn’t holding a mouse.

Nothing about the line looks wrong. That’s why it survives review.

A button is a handful of behaviors wearing some CSS

Styling is the part <button> does worst and the part everyone notices. Every browser ships an opinionated default that nobody keeps, so the first thing a design system does is reset it, and once the reset lands a <div> and a <button> are two identical rectangles.

That’s the trap. The visible difference is zero and the behavioral difference is most of what the element is for.

It’s focusable without a tabindex. It sits in the tab order at its position in the DOM, which means the sequence stays correct when the layout moves, nobody maintains a number, and nobody ever has to discover that a tabindex="3" left somewhere in a modal has been quietly reordering the whole page.

Enter and Space activate it, and activation dispatches a real click event. The handler you wrote for the mouse already serves the keyboard. There’s no second code path to keep in sync, which is the part that matters six months later when the handler changes.

Screen readers call it a button, because <button> carries an implicit role="button". Its text content becomes the accessible name at no cost.

disabled does three things at once: blocks the click, drops the element out of the tab order, and exposes the state to assistive technology. On a <div> that attribute is inert. It isn’t global, so the browser ignores it entirely and your [disabled] CSS selector ends up being the only thing in the system that responds to it, which looks correct and is the exact shape of the bug.

Inside a <form>, it submits. <button> defaults to type="submit". Free behavior or a surprise bug, depending on whether you knew. If a React form has ever reloaded the page on you, that was this.

Watch which events each control actually gets

Three controls below. One CSS class between them, so they’re the same rectangle, and the log records every event each one receives.

Try it

Put your mouse down. Press Tab until focus enters the demo, then keep tabbing, and press Enter and Space on whatever you land on. The log records every event the controls receive.

1 — bare div

Save

<div> plus a click listener.

2 — div with ARIA

Save

Plus role, tabindex, and a keydown handler.

3 — real button

No role, no tabindex.

Focus: somewhere else on the page

Event log — newest first

  1. Nothing yet.

The first thing to try is the boring one: press Tab and count how many stops there are. Panel 1 is not among them, and no amount of :hover styling changes that.

The more interesting result is on panel 3. Enter and Space don’t activate a native button the same way. Enter fires on the way down; Space waits until you let go. The log shows keydown Enter followed immediately by click, and then keydown Space, keyup Space, click in that order.

That split is user-agent behavior rather than something the HTML spec pins down keystroke by keystroke, so trust the log in front of you over this paragraph. I haven’t checked it in Safari.

The ARIA version is longer than the button and still isn’t equal to it

Panel 2 is what people write when they can’t change the tag:

<div class="btn" role="button" tabindex="0">Save</div>
control.addEventListener("keydown", (event) => {
  if (event.key !== "Enter" && event.key !== " ") return;
  // Without this, Space scrolls the page instead of pressing anything.
  if (event.key === " ") event.preventDefault();
  control.click();
});

event.key for the space bar is " ". A single space character. "Space" is the value of event.code, not event.key, and event.key === "Space" is a check that compiles, reads correctly, and never once passes.

Two attributes and five lines of JavaScript, and it’s still behind. disabled does nothing to it. It won’t submit a form. And the timing is wrong in a way you can see in the log: this version fires both keys on keydown, so Space activates on the way down here and on the way up on the real button.

You could fix that by splitting the handler across keydown and keyup. At that point you’ve written more code to imitate a button than the button costs, and you own every one of those lines the next time the behavior changes.

Where I’d still use a div

The case that actually holds up is a click target wrapping block content. Think of a card: an image, a heading, two lines of summary, a tag list, and a “Read more” link at the bottom, with the whole rectangle clickable. <button> takes phrasing content and must not contain interactive content, so the moment that link exists the card can’t be a button. Wanting it to be doesn’t change the content model.

The fix there still isn’t role="button" on the card. Put a real <button> or <a> on the title and stretch it over the card with an absolutely positioned ::after. The accessible name comes from the title text, the tab order is one stop instead of three, and everything in this post that the platform gives you for free still applies.

It costs you text selection across the card, because the overlay eats the drag. That’s a genuine loss and the reason I’d call this a trade rather than a fix.