DEV Community

Cover image for How Not to Create a Button

Posted on • Originally published at on

How Not to Create a Button

I have a habit of testing websites with a keyboard. I often encounter buttons that don't work as expected. It's frustrating, and I usually start digging into the source code to understand why the button isn't working. These reasons vary - some of them could be described as "normalized" patterns (I disagree), and some are really imaginative.

I'll share some ways to not create a button in this blog post. I'll also explain why those patterns aren't a good idea. You can find instructions on creating a button the right way in the last section, The Right Way to Create a Button.

Div as a Button

This div as a button pattern is by far the most common. Here's a code snippet to demonstrate it:

<div onClick="...">
  I pretend to be a button

Enter fullscreen mode Exit fullscreen mode

Sometimes these elements even have tabindex="0" to make them focusable with a keyboard. But they never, ever seem to have the keyboard event listeners for space and enter. This means that even if the user can focus on the so-called button, they can't activate it with a keyboard.

Another thing that is often missing is the role attribute to communicate that there is a button. When it's missing, non-sighted screen reader users won't even know any button exists. For them, that particular element is a focusable text they might encounter if they're browsing the page's content.

So, while this pattern works for mouse users, it's excluding most of the other user groups out there. It's possible to make a div work as a button for all these groups, but it's lots of work and makes the code harder to maintain. And you know, by using the button element, you'd get all that for free.

Link as a Button

Another HTML element I often see used as a button is the anchor-element, so, link. In these cases, it's styled to look like a button. However, underneath it all, it's a link:

<a href="...">
  I, too, pretend to be a button

Enter fullscreen mode Exit fullscreen mode

"But call to actions are a thing, right?" you might ask. Yeah, I know, they're a pattern that is widely used. And it's a bit confusing pattern - they often look like a button but then lead to another part of the website. And they don't initiate an action, such as opening a modal.

From the usage point of view, it doesn't make much difference for a sighted mouse user, whether it's a link or button under the hood. I mean, it works when clicking, be it a button or a link.

However, for non-mouse users, it does. For example, keyboard users have certain ways to interact with an element. They can activate a button with space and enter, and link only with enter. This means, that the so-called button is not actionable with space.

And why it is a problem? Well, have you ever pressed space on a site when it's not focused anywhere? It scrolls the page down. And that's what happens when you're focused on a link and press space. It's frustrating as user needs to scroll back up where they were. It might be even painful for some users in cases where every extra keystroke causes pain.

And then there's this: When users don't know that there is a link underneath the button, they don't have the control to use it as they would with a link. This means that they might, for example, want to open the link to a new tab or window.

Link with a Role of Button

<a role="button" tabindex="0" onClick={…}>
  I'm a link, I'm a button. Who am I?

Enter fullscreen mode Exit fullscreen mode

This one is my new favorite. I mean, it's a creative way of making a button even more confusing for many. For screen reader users, it indeed seems like a button. It even works if they try to activate it with screen reader commands, as they trigger the click-element. However, this type of button doesn't work with keyboard navigation.

Why, you ask? Isn't it a link, so it works with enter, right? The answer is no. That element is not a link because it no longer has the href-attribute. Removing the href-attribute strips away the semantics and keyboard interaction. This way, even the enter-key doesn't work with the element.

I think the most common reason for using this solution is libraries and frameworks hiding away the semantics of an element. My guess for this particular case is that there is some kind of CSS-in-JS-library in use, and the developer has needed the link's styles for a button. Then they have extended the link component and added all the attributes.

Button Element Wrapping a Link

Okay, the previous two examples are relatively common ones. The following two are (hopefully) not that popular.

First, I present to you a button element wrapping a link:

  <a href="...">
    I'm a link wrapped in a button. And I don't really work

Enter fullscreen mode Exit fullscreen mode

First, let's start with the fact that this is incorrect HTML. A link can't be inside a button, as the HTML specification defines the button's content model:

Phrasing content, but there must be no interactive content descendant and no descendant with the tabindex attribute specified.

The button-element in HTML-specification

Even though anchor-tag is phrasing content, it's also interactive, so this rules it out.

This pattern also does not work well for screen reader or keyboard users. Because the link is wrapped with a button, there's an extra tab stop before the user reaches the actual link. And as the button doesn't have a click-handler, trying to activate it doesn't do anything. So, the user feels like they're on a control, but it doesn't work, as the button still gets focused.

That is really frustrating and confusing for the user. If they try to go forward and tab to the link element and then press enter, they'll get the "button" to work. However, it is not clear that there is a next tab stop. And with the link, all the same, problems presented in the previous section remain.

For screen reader users, it presents even more problems. I tested this pattern with VoiceOver and couldn't get it to work at all. I'm not sure about the other screen readers, but I'd guess they also have problems.

Checkbox Label as a Button

This pattern has been the wildest of them all. And at the same time, I understand how a developer has ended up with this pattern. So, in this case, we have a checkbox-input, which is visually hidden, and then the label is styled to look like a button. The code:

<label htmlFor="checkbox">
  I also want to be a button
  checked />

Enter fullscreen mode Exit fullscreen mode

As mentioned, I understand how the developer ended up with this. There's this idea of changing something based on two states. The data that this particular checkbox represents can either be on or off. And based on that state, there are changes in the UI.

However, the checkbox serves another role in the UI. As Heydon Pickering mentions in Inclusive Components, users might suspect that they're also choosing a value for submission. This, however, is true only for screen reader users, as they hear that the underlying component is a checkbox.

The problem for sighted users is that as the label is styled to look like a button, they expect it's a button. But if they try to interact with it using a keyboard, it gets confusing. You see, the key that toggles a checkbox is space. So it doesn't work with enter at all.

So, after all these not-so-great ways of creating a button, let's look into making it the right way.

The Right Way to Create a Button

It's pretty simple. Use the <button>-element. It has everything out of the box: the role of a button to convey its semantics, tab index to put it into tab order, and easiness of giving it a click handler that handles the keyboard interaction as well.

<button onClick={...}>
  I am actual button

Enter fullscreen mode Exit fullscreen mode

This helps your and your colleagues' lives, as the code is easier to maintain. Also, using semantic elements helps the users - not everyone uses a mouse, so having the expected keyboard interaction is required. You don't want to exclude anyone, right?


Top comments (2)

merri profile image
Vesa Piittinen

The <a role="button" tabIndex={0} onClick={} onKeyDown={} /> has one use case: you have something that needs to be visually a link wrapping alongside text, with an action that does a button behavior. The reason for this is that you can never make <button /> element wrap like an inline element as display: inline; just simply does not work on a real button element. It always ends up as inline-block in minimum.

Of course it would be ideal to design things so that this would never happen, but everywhere I've been at there has eventually been a need for "look like a link, behave like a button" component. And the same way the other way around, "look like a button, behave like a link".

To avoid this we'd need to have the whole chain of things accounted for when creating things: the architecture of the site should go for progressive enhancement first, the designers should be aware of avoiding designing links that are not links, the developers should then be aware to work with progressive enhancement as their core principle which does clarify when to use a real button, and when it is a link. Docs should also exist to support doing things right. Break one part in this chain and the mess crawls back into existence, eventually.

billraymond profile image
Bill Raymond

Thank you for sharing this. I’m curious now if the hamburger menus so prevalent on sites is a cause for concern? For larger sites, they are usually off by default and only enable when stretched to mobile.

Many of them use an enabled or disabled checkbox to determine visibility and the interface is (I’m guessing) rarely a button.