Styling buttons, the right way

accessibility css html

If you’re building a website or a web app, you probably have buttons. And maybe links that look like buttons? Anyway, it’s important to get them right.

In this tutorial we’ll create basic styles for <a> and <button> elements, and a custom .btn CSS component. You will find a demo page for each step of the process.

Demo Section
Step 1 Resetting <button> styles
Step 2 Writing a “button” CSS component
Step 3 Styling hover, focus and active states
Step 4 Dealing with sticky focus styles

Resetting <button> styles

As a rule, 99.9% of the clickable things in your website or app should be either <a> or <button> elements. If you’re not sure what element to use in a given situation:

  1. If it goes to a different URL or changes most of the page’s content, use a link (<a href="some_url">…</a>).
  2. Otherwise, use a generic button (<button type="button">…</button>).

Using the correct element has a few advantages: it’s SEO-friendly (especially for links!), it works well with keyboard navigation, and it improves accessibility for all users.

In spite of this, developers rarely use the <button> element. All over the Web, we can see a lot of buttons that trigger JavaScript actions, and on closer inspection it turns out they’re coded with <div>, <span> or <a>.

Why is the <button> element so underused?

Thankfully, the styling part can be fixed!

/**
 * Reset button styles.
 * It takes a bit of work to achieve a neutral look.
 */
button {
  padding: 0;
  border: none;
  font: inherit;
  color: inherit;
  background-color: transparent;
  /* show a hand cursor on hover; some argue that we
  should keep the default arrow cursor for buttons */
  cursor: pointer;
}

We end up with buttons that look like regular text.

The downside of this approach is that now we have to style all our buttons, or users won’t recognize them (see: affordance). Another option is to use this style as a mixin (with Sass or another preprocessor) and apply it selectively:

@mixin button-reset {
  padding: 0;
  border: none;
  font: inherit;
  color: inherit;
  background-color: transparent;
  cursor: pointer;
}

.my-custom-button {
  @include button-reset;
  padding: 10px;
  background-color: skyblue;
}
<button type="button">
  I use default browser styles
</button>

<button type="button" class="my-custom-button">
  And I’m using custom styles instead
</button>

Writing a “button” CSS component

Now that we’ve removed default styles, let’s define our own button styles. This is what we’d like to do:

This calls for a CSS component. A CSS component is a style or collection of styles which we can apply using classes, often on top of a few different types of HTML elements. You may be familiar with this concept from CSS frameworks like Bootstrap or Foundation.

We’ll call this component .btn (like Bootstrap does, but we’ll restrict ourselves to a single color and size, to keep things simple).

.btn {
  /* default for <button>, but useful for <a> */
  display: inline-block;
  text-align: center;
  text-decoration: none;

  /* create a small space when buttons wrap on 2 lines */
  margin: 2px 0;

  /* invisible border (will be colored on hover/focus) */
  border: solid 1px transparent;
  border-radius: 4px;

  /* size comes from text & padding (no width/height) */
  padding: 0.5em 1em;

  /* make sure colors have enough contrast! */
  color: #ffffff;
  background-color: #9555af;
}

Here is what our button component looks like:

You may be wondering why contrast is such a big deal. That second line of buttons looks nice; who doesn’t like a pastel look?

Simply put: with good contrast, you can reach more users. Some of your users have imperfect vision. Others may be looking at your site on their phone, on the go, with the daylight making things harder to read. You can check your contrast ratios here, and read more about the UX of text contrast here.

Styling hover, focus and active states

It’s cool that your button looks nice, but… users are going to interact with it, and they will need visual feedback when the button’s state changes.

Browsers come with default styles for “focus” and “active” (i.e. pressed) states, but by resetting button styles we’ve removed some of those. We would also like to have styles for mouse-over, and overall we want styles that are visible and match our design.

Let’s start with a style for the :active state, triggered briefly when your button or link is clicked:

/* old-school "down" effect on clic + color tweak */
.btn:active {
  transform: translateY(1px);
  filter: saturate(150%);
}

We could change the button’s colors, but I want to reserve this effect for our mouse-over styles:

/* inverse colors on mouse-over */
.btn:hover {
  color: #9555af;
  border-color: currentColor;
  background-color: white;
}

Let’s now add some focus styles. Users of your websites or web apps can use the keyboard or virtual keyboard (on iOS and Android) to “focus” and activate form fields, buttons, links and other interactive elements.

In most Web projects I’ve seen, designers specify the expected mouse-over styles but not focus styles. What should we do? A simple solution is to reuse the :hover style as our :focus style:

/* inverse colors on mouse-over and focus */
.btn:hover,
.btn:focus {
  color: #9555af;
  border-color: currentColor;
  background-color: white;
}

And once you have this visible focus style (and not before!), you may want to remove the browser’s default styles for your button:

.btn {
  /* ... */
  /* all browsers: remove the default outline since
      we are rolling our own focus styles */
  outline: none;
}

/* Firefox: removes the inner border shown on focus */
.btn::-moz-focus-inner {
  border: none;
}

Try it out here; if you’re on a desktop computer, use the Tab and Shift+Tab keys to navigate between buttons:

Dealing with sticky focus styles

There is one tricky issue left. In several browsers, when you click a link or button, two pseudo-classes will apply:

The “active” pseudo-class stops applying as soon as you stop pressing the mouse button or trackpad. But in some browsers, the :focus style stays until the user clicks something else on the page.

In my tests, affected browsers include Chrome (66), Edge (16), and Firefox (60, only for links). Safari (11.1) seems to be smarter and avoid this issue.

We can fix this using the new :focus-visible pseudo-class (draft specification). This feature is still not fully specified, but the idea is that browsers should set the :focus-visible state only after keyboard or keyboard-like interaction, rather than on clicks.

Since it’s not yet implemented by browsers, we will have to use a JavaScript implementation, such as this polyfill. It operates on the whole page, and sets a focus-visible class on elements that received focus when using the keyboard only.

Let’s change our styles to decouple :hover and :focus styles:

/* inverse colors on hover */
.btn:hover {
  color: #9050AA;
  border-color: currentColor;
  background-color: white;
}

/* make sure we have a visible focus ring */
.btn:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(255, 105, 180, 0.5),
    0 0 0 1.5px rgba(255, 105, 180, 0.5);
}

Now, after we’ve included the focus-visible.js script in our page, it will add a js-focus-visible class to the <body> element. We can use this to remove focus styles from focused elements which do not have the focus-visible class:

/* hide focus style if not from keyboard navigation */
.js-focus-visible .btn:focus:not(.focus-visible) {
  box-shadow: none;
}

A simpler solution would be to declare focus styles for the focus-visible class only, but this would break if the polyfill is not active (e.g. if our JS failed to load).

This is our final result:

You can look at the final code to review everything we’ve seen in this tutorial.