Let’s get this out of the way right now: I don’t think there is a compelling reason to turn an unopinionated HTML element like a div or a span into a button. ’Cause, you know, button already exists.

However, the question was asked, “If you had to do it though, could it be done?”.

This post attempts to take you through the steps of how to, in some ways, convert an unopionated HTML element into a accessible ‘pseudo-button’, and hopefully convinces you to just use a button.

I’ve ruminated on the use of button element before. That was four years ago. My opinion today differs. I don’t care about the speed impact; a button is slower because it does more. What’s more, for the number of buttons you are going to have on almost any page/app it adds up to very little overhead in real terms. I don’t think this is a sensible place to make economies.

The button element gives you things you might not see or appreciate but others might.

button powers

What does a button give you that other elements don’t? Here are a few choice features:

A button can receive focus. For the uninitiated this means someone can move to that element easily with, for example, just a keyboard. It gets a tabIndex whether or not you set it.
It supports an accessKey so you can set a keyboard shortcut to simply activate it.
It supports the disabled attribute, which, when added means the button stops receiving clicks.
It has a type attribute, allowing it to submit the form it lives in, if the type is set to submit. It can also reset a form type="reset" or just do nothing type="button".
It has a value property, so you can conveniently set a different value than the text should you need it for scripting.

There are a bunch more features. If you’re interested, here are the details of the button element in the HTML 5.2 specification.

Meanwhile, an unopionated element, like a div gives you nothing. Hence the reason it’s demonstrably faster to render than a button.

Making a div more accessible

Hopefully that covers why you wouldn’t want to do this?

Well, as promised at the outset, we are going to see how far we can get anyway!

Say hello to our div:

<div class="btn">a div</div>

role=“button”

ARIA (Accessible Rich Internet Applications) attributes provide the means to make a div appear as a button to a screen reader. Here is the intro to the button role page on MDN:

Adding role=“button” will make an element appear as a button control to a screen reader. This role can be used in combination with the aria-pressed attribute to create toggle buttons.

Right, so now our button would look like this:

<div class="btn" role="button">a Div</div>

aria-pressed

In our instance the button is going to be a switch; it can be pressed or not. With ARIA we could also make it the control for something like a menu with aria-expanded but instead we will use aria-pressed. So default it will need that too:

<div class="btn" role="button" aria-pressed="false">a div</div>
aria-pressed isn’t a strict boolean, it can also be set to ‘mixed’ if considered partially presed.

tabindex

We need the buttons to be focusable. That means we need a tabindex. By default we want the button to follow the normal sequence so we can set it to “0”. If you are unfamiliar with tabindex you should know that a negative number means the element is focusable but not in the normal focus sequence/sequential keyboard navigation. You want this if you have a modal that you want focusable but only via script, where you would use focus() when appropriate.
A tabindex of “0” means the element is focusable and happy to follow the normal order. A positive value e.g “1” provides an indication to the browser of how that element should be prioritised in the focus order. If you had elements with tabindex 1,2,3 respectively, your intention would be for the browser to focus them in ascending order. First 1, then 2, then 3. Note, it’s just an indication, not a promise! You should also avoid doing that unless you absolutely know you need to though; it’s liable to create an unpredictable experience for keyboard users. Just go with The Web’s Grain.

Right, with the tabindex attribute in, we are now looking like this:

<div class="btn" role="button" aria-pressed="false" tabindex="0">a div</div>

keyboard event handlers

If you have a button element that has focus, pressing space, or enter will activate the button. When you convert a different element with role="button" you don’t get that functionality for free. You need to add your own keyDown listeners. You can add the listener to individual elements or, thanks to event bubbling, you can listen on a containing element. For example:

wrapper.addEventListener("keydown", e => {
    if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") {
        toggleBtn(e.target);
    }
});

disabled (updated – thanks Darek)

The button element has a disabled state built in. When this is present, keyboard actions on the button are ineffective. For a pseudo-button you would need to add this functionality. Stylistically, this could be achieved easily enough. Something like:

[aria-disabled] {
    pointer-events: none;
    opacity: 0.5;
}

However, you would still need to add the correct aria-* attribute, so that assistive technology knows what to do with the element. Without aria-disabled applied to the element it would only be visually ‘disabled’; not functionally.

Styles

There is no way point dancing around this – the button element is not straightforward to style. Browsers have opinions on how a button should look and you need to consider that.

However, switching out a button for a pseudo-button isn’t going to negate that need. Once an element has role="button" it is focusable and as a consequence, browsers indicate this focus in the same manner they would a real button. You will need to consider focus states and styles for when a button is toggled. If you are switching off the default outline for example, make sure you are adding a suitable sustitute.

Example pseudo-buttons

The MDN role="button" documentation gives solid examples that I have amended for this example. Here are a number of divs in a wrapping div. Pressing “Convert to pseudo-buttons” decorates the divs with the necassary attributes and event listeners to give us some of buttons goodnes. You can tab across them and pressing space or enter when one is selected toggles the aria-pressed attribute to indicate the state of the button.

View it on Codepen

Or you can view the embed here:

See the Pen
Making a div a button with aria
by Ben Frain (@benfrain)
on CodePen.

Summary

Makming unopionated elements into pseudo-buttons is not a trivial task. A lot of work has to be done, and therefore going forward, managed, to give pseudo-elements just some of the functionality built into the native button element.

There are inherent challenges and arguably shortcomings in dealing with buttons in some situations but appreciate that the friction is likely due to the fact that you are going against the grain of the medium you are working with.

Despite any frustrations, I’d suggest the end result will be far smoother if you choose to work with the grain instead.