The HTML button tag is a mess

Tom Golden
Tom Golden

What happens when you click a button?

You would hope it would:

  • fire its click event listeners
  • receive the browser focus
  • submit its parent <form> if it was given type="submit"

What if you don't use a pointing device?

You would hope they could:

  • focus the button with the keyboard
  • simulate a button click (as specified above) by pressing either Enter or Space


Except... the <button> tag doesn't do this.

At least, not consistently across all the major browsers.

Here's a link to a breakdown of different browsers input compatibility inconsistencies. There's a lot there, because there's a lot of inconsistencies...

So how do we do it?

I explored a few alternatives and came to realize that the least worst solution is to use <div tabindex="0">, with event listeners to "polyfill" (read: wrap with secret functions) the default behavior of the <button> tag.

Accessible buttons aren't particularly sexy, so here's some example React code with some comments that highlight the main ideas behind an abstracted solution:

const React, { Fragment, useRef } from 'react';
const { number, string, func, bool } from 'prop-types';

const Button = ({
  onClick: _onClick,
  onKeyDown: _onKeyDown,
}) => {
  const hiddenInputEl = useRef(null);

  // wrap click event to not fire the callback when disabled
  const onClick = ev => {
    if (!disabled) {
      // clicks the hidden input, if it has one - e.g. to trigger a form submit
      if (hiddenInputEl.current && {;

  // wrap keydown event to call onClick when Enter or Space is pressed
  const onKeyDown = ev => {
    const { preventDefault } = ev;
    let preventedDefault = false;
    ev.preventDefault = (...args) => {
      preventedDefault = true;
      preventDefault.apply(ev, args);
    if (!preventedDefault && [' ', 'Enter'].includes(evt.keyCode)) {

  return (
        (type === 'button')
          ? null
          : (
            // hidden input to polyfill type="submit" and type="reset" behavior
                position: 'absolute',
                height: 1,
                width: 1,
                opacity: 0,
                overflow: 'hidden',
                pointerEvents: 'none',
                clip: 'rect(1px, 1px, 1px, 1px)'

Button.defaultProps = {
  tabIndex: 0,
  role: "button",
  type: "button",
  onClick: () => {},
  onKeyDown: () => {},
  disabled: false

Button.propTypes = {
  tabIndex: number.isRequired,
  role: string.isRequired,
  type: string.isRequired,
  onClick: func.isRequired,
  onKeyDown: func.isRequired,
  disabled: bool.isRequired

If there's an error with the code, or you have some additional suggestions please reach out to me by email and I'll get back to you! :)

Soccer Explained for Americans

A refinement of a video of the same name

Don't use CSS minifiers with SASS

CSS minifier/compressor benchmarks and the cold-hearted reality