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 and active states
Step 4 Managing 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?

  • Knowledge: many developers don’t know about it (learning HTML’s 100+ elements takes a little while).
  • Styling: <button> comes with complex default styles, which can make it hard to achieve a custom look.

Thankfully, the styling part can be fixed!

/**
 * Reset button styles
 * It takes a little bit of work to achieve a “blank slate” look.
 */
button {
  padding: 0;
  border: none;
  font: inherit;
  color: inherit;
  background-color: transparent;
  /*
    Show a hand cursor on mouse-over instead of the default arrow cursor.
    (Some argue that we should keep the default arrow cursor for buttons, to be consistent with how desktop operating systems treat 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.

If we want to avoid that, 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:

  • a “button” style that can be applied to both links or buttons;
  • we want to apply it selectively, because we’ll have other link and button styles in our pages.

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 mouse-over) */
  border: solid 2px transparent;
  border-radius: 0.4em;

  /* 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. Or bad screens that make washed out colors hard to differentiate. Others may be looking at your site on their phone, in broad daylight. You can check your contrast ratios here, and read more about the UX of text contrast here.

Styling hover 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 states like “hover” (mouse pointer is hovering the element) and “active” (the element is being pressed); and 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:

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

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

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

Here’s our result — try out the active and hover styles. In the next section, we’ll deal with focus styles.

Managing focus styles

What are focus styles?

Users of your websites or web apps can use a keyboard or some other input software or device (gamepads, speech input software, head pointers, motion tracking or eye tracking, single switch entry devices, etc.) to navigate the page. Those methods will move the current “focus” from one element to the next, so that the user may activate an interactive element or type in a focused text field.

Even if you primarily use a mouse or trackpad, you might still be using the Tab key when using a web form, to jump from one form field to the next. Even your mouse users are keyboard users some of the time.

To serve all users, we need the currently focused element to be clearly visible. Thankfully, browsers do it by default:

  • Historically, browsers like Internet Explorer and older versions of Firefox had a very subtle focus style: a thin dotted outline. This led to the recommandation to add your own custom and more accessible focus style.
  • In recent years, Chrome, Edge, Firefox and other browsers ship with a default focus style that uses a double outline with two colors, which is more often accessible on varied backgrounds.

If you’re not sure what to do, you can keep the browser’s default style. But if you do want to customize it, read on.

Custom focus styles

Let’s define a custom focus style with the aptly-named :focus pseudo-class. We’d like this style to follow accessibility guidelines, including:

  1. WCAG 2.1 (Recommendation) Success Criterion 2.4.7 Focus Visible
  2. WCAG 2.2 (Draft): Understanding Success Criterion 2.4.11: Focus Appearance (Minimum)

When a user interface component has keyboard focus, the focus indicator:

  • Encloses the visual presentation of the user interface component;
  • Has a contrast ratio of at least 3:1 between its pixels in the focused and unfocused states;
  • Has a contrast ratio of at least 3:1 against adjacent colors.

So we basically need a kind of outline or border, and it should be contrasted enough against part of the element itself and its environment, which can be a bit hard to always achieve. For instance, if you have a button with a gray border on a white page, and you just want to change the border’s color when that button is focused, picking a color can be tricky:

.btn {
  border: solid 1px gray;
}

.btn:focus {
  /* hide the browser's default outline, because we're showing our own focus styles */
  outline-color: transparent;

  /* ❌ blue is contrasted enough with the white background (8.51:1), but not contrasted enough with the border's initial gray color (2.42:1) */
  border-color: blue;

  /* ✅ black is different enough from both white (21:1) and gray (5.92:1) */
  border-color: black;
}

And when you’re not sure what the surrounding background color is going to be, it can be even harder to ensure your focus styles have good contrast every time.

To make our focus styles always perceptible, we can try creating a double-outline effect like the default focus style of Chrome and Firefox. Because the outline property only accepts a single outline, we’ll have to use a combination of outline and box-shadow to achieve the same effect.

.btn:focus {
  /* paint a 2px white pseudo-outline using box-shadow */
  box-shadow: 0 0 0 2px #fff;
  /* then paint a 2px dark pink outline and offset it to reveal one or two pixels of the pseudo-outline */
  outline: solid 2px #d59;
  outline-offset: 1px;
}

That works pretty well. And since we are setting a custom outline value, we don’t have to do anything like outline-color: transparent or outline: none to override the default outline style.

Sticky focus styles

There is one tricky issue left. When you click a link or button, two states will apply:

  • :active;
  • :focus.

The :active state stops as soon as you stop pressing the mouse button or trackpad. But in most browsers, clicking an interactive element gives it focus, so the :focus style applies until the user clicks somewhere else on the page.

This has been a headache for many web designers and developers for almost two decades. I cannot count the number of clients who have told me “when I click here, there is a strange border around the button, please remove it!”

Even if you know that this strange border helps make their site accessible, convincing a client to leave it be can be an uphill battle! (And if you don’t know, sadly you’ll find plenty of blogs, forum posts and StackOverflow answers telling you to “just use outline: none” and call it a day.)

Thanksfully there is a better way, implemented in all browsers in recent years: the :focus-visible pseudo-class.

How it works in a nutshell: for buttons and links, browsers will set the :focus-visible state on an element after a keyboard or keyboard-like interaction, but not after a click.

We can replace :focus with :focus-visible to fix our issue with focus styles sticking around after a click:

.btn:focus-visible {
  /* paint a 2px white pseudo-outline using box-shadow */
  box-shadow: 0 0 0 2px #fff;
  /* then paint a 2px dark pink outline and offset it to reveal one or two pixels of the pseudo-outline */
  outline: solid 2px #d59;
  outline-offset: 1px;
}

Note that Web browsers have recently moved to use :focus-visible for their own default focus styles. In that case, we don’t need to do anything about default focus styles, because our custom :focus-visible style will override them.

And if some browsers are showing focus styles on :focus instead of :focus-visible, we can use this style to make sure that only our custom focus styles would show up, and only on :focus-visible:

/* disable the default outline on a focused element which doesn’t have the :focus-visible state (e.g. a button after a mouse click) */
.btn:focus:not(:focus-visible) {
  outline-color: transparent;
}

And with that, here is our our final result:

Go ahead and look at the final code to review everything we’ve seen in this tutorial.