loading...
Cover image for CSS Button Styling Guide

CSS Button Styling Guide

5t3ph profile image Stephanie Eckles Updated on ・7 min read

Modern CSS Solutions to Old CSS Problems (18 Part Series)

1) Keep the Footer at the Bottom: Flexbox vs. Grid 2) Equal Height Elements: Flexbox vs. Grid 3 ... 16 3) CSS-Only Full-Width Responsive Images 2 Ways 4) Pure CSS Smooth-Scroll "Back to Top" 5) Totally Custom List Styles 6) Animated Image Gallery Captions with Bonus Ken Burns Effect 7) CSS-Only Accessible Dropdown Navigation Menu 8) ✨ Announcing ModernCSS.dev 9) Solutions to Replace the 12-Column Grid 10) CSS Button Styling Guide 11) Icon Button CSS Styling Guide 12) Resource: The Complete Guide to Centering in CSS 13) Generating `font-size` CSS Rules and Creating a Fluid Type Scale 14) Container Query Solutions with CSS Grid and Flexbox 15) Expanded Use of `box-shadow` and `border-radius` 16) 3 CSS Grid Techniques to Make You a Grid Convert 17) 3 Popular Website Heroes Created With CSS Grid Layout 18) Announcing Style Stage: A Community CSS Showcase

This is the ninth post in a series examining modern CSS solutions to problems I've been solving over the last 13+ years of being a frontend developer. Visit ModernCSS.dev to view the whole series and additional resources.

This guide will explore the ins and outs of styling an accessible, extensible button appearance for both link and button elements.

Topics covered include:

  • reset styles for a and button
  • display, visual, size, and text styles
  • accessible styling considerations
  • extended styles for common scenarios

Oh, the button (or is it a link?). I've battled the button since the days of hover delay from waiting for a second image to load, through image sprites, and then was immensely relieved when border-radius, box-shadow and gradients arrived on the scene.

But... we took button styling too far, and somewhere along the way completely lost sight of what it really means to be a button, let alone an accessible button (or link).

STOP! Go read this excellent article: Links vs. Buttons in Modern Web Applications to understand when it's appropriate to use a versus button

We'll look at what properties are required to visually create a button appearance for both a and button, and additional details required to ensure they are created and used accessibly.


Reset Default Styles

Here's our baseline - native browser styles as rendered in Chrome, with the only changes so far being the link is inheriting the custom font set on the body, and I've bumped the font-size as well:

default link and button styles

The HTML if you're playing along at home is:

<a href="javascript:;">Button Link</a>
<button type="button">Real Button</button>

I've used the javascript:; string for the href value so that we could test states without triggering navigation. Similarly, since this button is not for a form submit, it needs the explicit type of button to prevent triggering a get request and page reload.

Reset Styles

Note: Typically I apply the Normalize reset to CodePens, but for this lesson we are starting from scratch to learn what is required to reset for buttons and links. Use of Normalize or other popular resets do some of these things for you.

First, we'll add the class of button to both the link and the button just to emphasize where styles are being applied for this lesson.

<a href="javascript:;" class="button">Button Link</a>
<button type="button" class="button">Real Button</button>

box-sizing

Ensure your styles include the following reset - if you don't want it globally (you should) you can scope it to our button class.

* {
  box-sizing: border-box;
}

In a nutshell, this rule prevent things like borders and padding from expanding the expected element size (ex. a 25% width remains 25%, not 25% + border width + padding).

a

For the link, we only have one reset to do:

a.button {
  text-decoration: none;
}

This simply removes the underline.

button

Next, we have a few more rules required to reset the button:

button.button {
  border: none;
  background-color: transparent;
  font-family: inherit;
  padding: 0;
  cursor: pointer;

  @media screen and (-ms-high-contrast: active) {
    border: 2px solid currentcolor;
  }
}

There are some differences in the display value as well between browsers, but we're going to change it to a unique option shortly.

With these reset styles, we now have this appearance:

link and button with reset styles

Thanks to @overflowhidden for providing a solution to ensure a perceivable button border for users with Windows High Contrast mode enabled.

Display Styles

What I have found to work best across many scenarios is display: inline-flex which gives us the content alignment power of flexbox but sits in the DOM within inline-block behavior.

a.button,
button.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

Flex alignment comes in handy should you add icons in the future, or impose width restrictions.

Visual Styles

Next we'll apply some standard visual styles which you can certainly adjust to your taste. This is the most flexible group of styles and you can leave out box-shadow and/or border-radius.

$btnColor: #3e68ff;

a.button,
button.button {
  // ... existing styles
  background-color: $btnColor;
  color: #fff;
  border-radius: 8px;
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18);
}

Now our link and button are starting to look more alike:

link and button with visual styles

Button Contrast

There are two levels of contrast involved when creating initial button styles:

  1. At least 3:1 between the button background color, and the background it is displayed against
  2. At least 4.5:1 (for text less than 18px or 16px bold) or 3:1 (for text greater than those measures) between the button text and the button background

Assuming a white page background, our button color choice passes with 4.54:1.

Size

We intentionally left out one property under the "Visual" categorization that you might have missed upon seeing the progress screenshot: padding.

Since padding is part of the box-model, we left it for the size section.

Let's apply the size values and then discuss:

a.button,
button.button {
  // ... existing styles
  padding: 0.25em 0.75em;
  min-width: 10ch;
  min-height: 44px;
}

We apply padding using em units, which is a preference that allows the padding to proportionally resize with the applied font-size.

Next, we set a min-width using the ch unit, which is roughly equal to the width of the 0 character of the applied font and font-size. This recommendation is a visual rhythm guardrail. Consider the scenario you have two side-by-side buttons with one short and one longer label, ex. "Share" and "Learn More". Without min-width, the "Share" button would be abruptly shorter than "Learn More".

The min-height is based on ensuring the button is a large enough target on touch devices to meet the WCAG 2.1 success criteria for 2.5.5 - Target Size.

The styles are starting to come together, but we're not done yet:

link and button with size styles

Text Styles

Based on the last progress screenshot, you might be tempted to skip text styles.

But look what happens when we reduce the viewport size and trigger responsive behavior:

link and button within reduced viewport

As you can see, we have different alignment and the line-height could be adjusted as well.

I intentionally skipped fixing text alignment in the reset styles, so we'll now make sure it's centered for both. Then we can also reduce the line-height - this may need adjusted depending on the font in use.

a.button,
button.button {
  // ... existing styles
  text-align: center;
  line-height: 1.1;
}

Alright, looking great!

link and button with text styles

State Styles

Right now, the only visual feedback a user receives when attempting to interact with the buttons is the cursor changing to the "pointer" variation.

There are three states we need to ensure are present.

:hover

The one that usually gets the most attention is hover, so we'll start there.

A typical update on hover is changing the background color. Since we were fairly close to 4.5, we will want to darken the color.

We can take advantage of Sass to compute this color for us using the $btnColor variable we defined in the "Visual" section:

a.button,
button.button {
  // ... existing styles
  &:hover {
    background-color: scale-color($btnColor, $lightness: -20%);
  }
}

The effect is a little jarring, but we have another modern CSS tool to soften this, aptly named transition. The transition property will need to be added outside of the hover rule so that it applies both on "over" and "out".

a.button,
button.button {
  // ... existing styles

  transition: 220ms all ease-in-out;

  // ...&:hover
}

demo of hover transition

:focus

For keyboard users, we need to ensure that the focus state is clearly distinguishable.

By default, the browsers apply a sort of "halo" effect to elements that gain focus. A bad practice is simply removing the outline property which renders that effect and failing to replace it.

We will replace the outline with a custom focus state that uses box-shadow. Like outline, box-shadow will not change the overall element size so it will not cause layout shifts. And, since we already applied a transition, the box-shadow will inherit that for use as well for an extra attention-getting effect.

a.button,
button.button {
  // ... existing styles

  // ...&:hover

  &:focus {
    outline-style: solid;
    outline-color: transparent;
    box-shadow: 0 0 0 4px scale-color($btnColor, $lightness: -40%);
  }
}

Once again, we have used the scale-color function, this time to go even a bit darker than the hover color. This is because a button can be in both the hover and focus states at the same time.

demo of link and button focus

Thanks to @overflowhidden for providing a solution to ensure a perceivable :focus state for users with Windows High Contrast mode enabled.

:active

Lastly, particularly for the "real button", it is best to define an :active state style.

For links this appears for a brief moment during the "down" of a click/tap.

For buttons, this can be shown for a longer duration given that a button can be triggered with the space key which can be held down indefinitely.

We will append :active to our existing :hover style:

&:hover,
&:active {
  background-color: scale-color($btnColor, $lightness: -20%);
}

Style Variations

The topic of outlined ("ghost") buttons is a topic for a different day, but there are two variations that we'll quickly add.

Small Buttons

Using BEM format, we'll create the button--small class to simply reduce font size. Since we set padding to em, that will proportionately resize. And our min-height will ensure the button remains a large enough touch target.

&--small {
  font-size: 1.15rem;
}

Block Buttons

There may be times you do want block behavior instead of inline, so we'll add width: 100% to allow for that option instead of changing the display prop since we still want flex alignment on the button contents:

&--block {
  width: 100%;
}

Gotcha: Child of Flex Columns

Given the scenario the button is a child of a flex column, you may be caught off guard when the button expands to full-width even without the button--block class.

To future-proof against this scenario, you can add align-self: start to the base button styles, or create utility styles for each of the flex/grid alignment property values: start, center, and end.

Demo

Modern CSS Solutions to Old CSS Problems (18 Part Series)

1) Keep the Footer at the Bottom: Flexbox vs. Grid 2) Equal Height Elements: Flexbox vs. Grid 3 ... 16 3) CSS-Only Full-Width Responsive Images 2 Ways 4) Pure CSS Smooth-Scroll "Back to Top" 5) Totally Custom List Styles 6) Animated Image Gallery Captions with Bonus Ken Burns Effect 7) CSS-Only Accessible Dropdown Navigation Menu 8) ✨ Announcing ModernCSS.dev 9) Solutions to Replace the 12-Column Grid 10) CSS Button Styling Guide 11) Icon Button CSS Styling Guide 12) Resource: The Complete Guide to Centering in CSS 13) Generating `font-size` CSS Rules and Creating a Fluid Type Scale 14) Container Query Solutions with CSS Grid and Flexbox 15) Expanded Use of `box-shadow` and `border-radius` 16) 3 CSS Grid Techniques to Make You a Grid Convert 17) 3 Popular Website Heroes Created With CSS Grid Layout 18) Announcing Style Stage: A Community CSS Showcase

Posted on May 7 by:

5t3ph profile

Stephanie Eckles

@5t3ph

Girl Geek, Web Dev (frontend/JS/React) building a design system, teaching a code video series, authoring ModernCSS.dev, egghead instructor, mom to 2 girls

Discussion

markdown guide
 

"Similarly, since this button is not for a form submit, it needs the explicit role of button to prevent triggering a get request and page reload."

I think you mean for that purpose, it needs type="button", not role="button"

 

You are correct! I was working on this too late and got my wires crossed :) Updated, thanks!

 

Great article, Stephanie!

I had a question for you. I am currently working on a book about CSS and it's tentatively entitled "Modern CSS". I was not aware of your site moderncss.dev before I chose that name. If you have an issue with that, please let me know and I will gladly change the name!

 

You're very gracious to ask! I'll message you