Everything you didn’t know you need to know about buttons

Steve Sewell - Jan 31 '23 - - Dev Community

Show you use an anchor tag, a button tag, or something else entirely (a div??) for clickable elements in HTML?

// 🚩
export function MyButton() {
  return <div onClick={...}>Click me</div>
}
//❓
export function MyButton() {
  return <button onClick={...}>Click me</button>
}
//❓
export function MyButton() {
  return <a onClick={...}>Click me</a>
}
Enter fullscreen mode Exit fullscreen mode

The answer is surprisingly nuanced, and some of it might surprise you.

The problem with divs

Let’s start by making one thing clear - you should not use divs for clickable elements (at least in 99% of cases). But why?

Simply put, a div != a button. A div is simply a generic container, and is missing a number of qualities that a properly clickable element should have, such as:

  • Divs are not focusable, for instance your tab key will not focus a div like it will for any other button on your device
  • Screen readers and other assistive devices don’t recognize divs as clickable elements
  • Divs do not translate certain key inputs, like space bars or return keys, to clicks when focused

Now, you can work around some of these issues with a few attributes like tabindex="0" and role=”button”:

// 🚩 Trying to make a div *mostly* behave like a button...
export function MyButton() {
  function onClick() { ... }
  return (
    <div
      className="my-button"
      tabindex="0" // Makes the div focusable
      role="button" // Hint to screen readers this is clickable
      onClick={onClick}
      onKeydown={(event) => {
        // Listen to the enter and space keys when focused and call the
        // click handler manually
        if (event.key === "Enter" || event.key === "Space") {
          onClick()
        }
      }}
    >
      Click me
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Oh yeah, and we need to make sure to style the focused state so there is user feedback that this element was focused too. And we must make sure this passes all accessibility concerns, like:

.my-button:focus-visible {
  outline: 1px solid blue;
}
Enter fullscreen mode Exit fullscreen mode

This is becoming a lot of work to try and chase down all of the nuanced (but critical) behaviors of buttons and implement it all manually.

But we’re in luck, because there is a better way (most of the time)!

The button tag to the rescue

The beauty of the button tag is it behaves just like any other button on your device, and is exactly what users and accessibility tools expect.

It is focusable, accessible, keyboard inputable, has compliant focus state styling, the works!

// ✅
export function MyButton() {
  return (
    <button onClick={...}>
      Click me
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

But, buttons come with a couple problems we need to be aware of.

Problems styling buttons

The biggest annoyance I’ve always had with buttons is styling them.

For instance, if you simply want to give a button a nice light purple background:

<button class="my-button">
  Click me
</button>
<style>
  /* 🤢 */
  .my-button { 
    background-color: purple; 
  }
</style>
Enter fullscreen mode Exit fullscreen mode

You will end up with this atrocity:

A very ugly button

That looks right out of Windows 95.

Yes, browsers try to force all kinds of weird styling to button elements and applying your own styles just makes a mess.

This is why we all love divs. They come with no added styling or behavioral baggage. They work and look exactly as expected, every time.

Now you could say, oh! appearance: none will reset the appearance! But no, that does not quite do what you think.

<button class="my-button">
  Click me
</button>
<style>
  .my-button { 
    appearance: none; /* 🤔 */
    background-color: purple; 
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Our monster still remains:

A very ugly button

Fixing button styling

That’s right, we have to actually reset properties line by line:

/* ✅ */
button {
  padding: 0;
  border: none;
  outline: none;
  font: inherit;
  color: inherit;
  background: none
}
Enter fullscreen mode Exit fullscreen mode

This will now finally give us a button that looks and behaves like a div, with one additional benefit that it still uses the browsers default focus styling.

An alternative option you have is to use all: unset to get back to no special styling in one simple property:

/* ✅ */
button { all: unset; }
button:focus-visible { outline: 1px solid var(--your-color); }
Enter fullscreen mode Exit fullscreen mode

Just don’t forget to add your own focus state (e.g. an outline with your brand color, assuming it has sufficient contrast), and you’re solid.

Fixing form behaviors of buttons

There is one last issue to be aware of when using the button tag. Any button inside of a form by default is treated as a submit button, and when clicked will submit the form. What??

function MyForm() {
  return (
    <form onSubmit={...}>
      ...
      <button type="submit">Submit</button>
      {/* 🚩 Clicking "Cancel" will also submit the form! */}
      <button onClick={...}>Cancel</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

That’s right, the default type attribute for buttons is submit. Yeah, it’s weird. And annoying.

To fix this, unless your button actually is mean to submit a form, always add type="button" to it, like so:

export function MyButton() {
  return (
    <button 
      type="button" // ✅
      onClick={...}>
       Click me
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

And now our buttons will no longer attempt to find their closest form parent and submit it. Whew, that almost got weird.

Linking to other pages

Here is the one big exception to our rule. We do not want to use buttons for links to other pages:

// 🚩
function MyLink() {
  return (
    <button
      type="button"
      onClick={() => {
        location.href = "/"
      }}
    >
      Don't do this
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

A few problems with buttons that use click events to link to pages:

  • They are not crawlable (so very bad for SEO)
  • Users cannot open this link in new tabs or windows (e.g. with cmd/ctrl click, right click “open in new tab”, etc)

As a result, let’s not use buttons for navigation. That’s where we want our good ole a tag.

// ✅
function MyLink() {
  return (
    <a href="/">
      Do this for links
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

And the best part, they have all of the above mentioned benefits of buttons (accessible, focusable, keyboard inputtable, etc) and they don’t come with a bunch of funky styling!

But, before you say waaait, if an a has the benefits of a button, without all the funky styling, should we not just use them for anything clickable and save ourselves some headaches?

// 🚩
function MyButton() {
  return (
    <a onClick={...}>
      Do this for links
    </a>
  )
}
Enter fullscreen mode Exit fullscreen mode

Well, no. That’s because an a tag without an href no longer behaves like a button. That’s right, it only has the full button behaviors, such as being focusable, when it also has an href with a value.

So, let’s be sure to stick to buttons for buttons, and anchors for links.

Putting it all together

One pattern I quite like is to encapsulate these rules in a component, so you can simply use your MyButton component and if you provide a URL, it becomes a link, otherwise is a button, like so:

// ✅
function MyButton(props) {
  if (props.href) {
    return <a class="my-button" {...props} />
  }
  return <button type="button" class="my-button" {...props} />
}

// Renders an <a href="/">
<MyButton href="/">Click me</MyButton>

// Renders a <button type="button">
<MyButton onClick={...}>Click me</MyButton>
Enter fullscreen mode Exit fullscreen mode

This way, we can have a consistent developer experience and user experience, regardless of if the purpose of the button is a click handler or a link to another page.

Conclusion

Ok, that was a lot! In short: for links, use an anchor tag with the href property, for all other buttons use the button tag with type="button".

About me

Hi! I'm Steve, CEO of Builder.io.

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.

So this:

import { BuilderComponent, registerComponent } from '@builder.io/react'
import { Hero, Products } from './my-components'

// Dynamically render compositions of your components
export function MyPage({ json }) {
  return <BuilderComponent content={json} />
}

// Use your components in the drag and drop editor
registerComponent(Hero)
registerComponent(Products)
Enter fullscreen mode Exit fullscreen mode

Gives you this:

Gif of Builder.io

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player