Accessible Buttons vs. Divs That Pretend to Be Buttons
A div with a click handler looks like a button and fails like one everywhere you can't see.
There’s a pattern I see in scan after scan, on sites built by competent teams: something that looks exactly like a button, works exactly like a button when you click it with a mouse, and is, in the HTML, a <div> with a click handler. I want to take this pattern apart carefully, because the interesting thing about it isn’t that it’s wrong. It’s how much invisible machinery you have to rebuild once you leave the real element behind, and how nobody ever rebuilds all of it.
Start with what a real <button> gives you, silently, the moment you type it. It’s focusable: pressing Tab reaches it. It’s activatable by keyboard: both Enter and Space press it, and Space has a subtlety, it fires on release, and scrolling is suppressed while it’s held, tiny behaviors you have never thought about because the browser has always handled them. It announces itself: a screen reader says “button,” so the user knows this is a thing that does something, as opposed to text that merely sits there. It participates in forms, submitting them when appropriate. It respects the disabled attribute, dropping out of the tab order and refusing clicks. And it works with every input device the browser supports, including ones you’ve never tested with: switch controls, voice control software where a user says “click Subscribe” and the software finds the element because it is, in fact, a button.
A <div> with an onclick has none of this. It’s not in the tab order, so keyboard users can’t reach it. If they could reach it, Enter and Space would do nothing, because divs don’t translate key presses into clicks. A screen reader reads its text but gives no hint it’s interactive; “Subscribe” announced as plain text is a dead end, not an invitation. Voice control may fail to target it. The click works, the appearance is perfect, and everything underneath the appearance is hollow.
Now, you can patch a div. This is where it gets instructive. Add tabindex="0" and it becomes focusable. Add role="button" and screen readers announce it correctly. Add a keydown handler that responds to Enter and to Space, remembering that Space should also not scroll the page, and keyboard activation works. Handle the disabled case yourself, since disabled means nothing on a div, which means also managing aria-disabled and removing it from the tab order conditionally. Each patch is small. But notice what you’re doing: reimplementing, by hand, in JavaScript, behaviors the browser gives away free, with a spec you’re reconstructing from memory. The ARIA specification itself is blunt about this; the first rule of ARIA, in essence, is don’t use ARIA if a native element already does the job. role="button" is a promise to assistive technology that the element behaves like a button. The attribute doesn’t make the promise true. Your code does, or fails to, and it usually fails somewhere, because the full behavior of a button is longer than anyone’s checklist.
It’s worth asking how this pattern happens, because nobody sets out to fake a button. Sometimes it’s styling: years ago, buttons were genuinely annoying to restyle across browsers, and the habit of reaching for a div outlived the problem, which CSS solved long ago; a <button> today can be styled into anything. Sometimes it’s frameworks: component libraries that render clickable divs, tutorials that demonstrate onClick on whatever element was handy. Sometimes it’s drift: a thing starts as a non-interactive card, someone makes the whole card clickable, and no one revisits the markup. All three roads lead to the same place, markup that describes decoration while the behavior lives somewhere the browser can’t see.
And that’s the general principle hiding under this narrow-sounding topic. HTML is not a drawing format; it’s a description of what things are, and an entire ecosystem of software reads the description and acts on it. Semantics is the practical name for this. A <button> doesn’t just look pressable, it is declared pressable, and everything downstream, keyboards, screen readers, voice software, browser extensions, your own future test scripts, leans on the declaration. When the declaration is missing, every one of those consumers fails independently, each in front of a different user you’ll never hear from. A quick self-audit is clarifying: click something interactive on your site and check what element it really is. The rule of thumb is short enough to remember. If it navigates somewhere, it’s a link, <a> with an href. If it does something on the page, it’s a <button>. If it’s neither, it probably shouldn’t have a click handler at all.
The economics, finally, are lopsided in the way accessibility economics usually are. Writing <button> instead of <div> costs nothing; it’s the same number of keystrokes. The div costs you the users who can’t click a mouse precisely, the users on screen readers, the users driving by voice, and it costs them at the worst spot, since the elements that get faked are the important ones, the add-to-cart, the submit, the subscribe. When GazeSite flags a fake button, the remedy line is almost embarrassingly short: make it a button. Most findings in an audit ask you to add something. This one just asks you to stop pretending, and let the browser do the enormous amount of work it was already offering to do.
More articles
Website scores only mean something relative to sites competing for the same visitors, and different industries fail in characteristically different ways.
Read →Accessibility arguments fail when framed as compliance or virtue; they succeed when framed as ordinary business investment.
Read →Accessibility problems compound like technical debt, and the interest is paid in customers you never see.
Read →