Simplify your button CSS and avoid variant specificity issues.

Managing button styles in CSS can be a daunting task, especially as the number of variations and modifiers grows. This can lead to an ever-increasing complexity and potential specificity issues that make your code bloated, confusing, and difficult to manage. Adding state to the mix only adds another layer of complexity, resulting in a lot of repetitive, conflicting, and confusing CSS to cover all the potentialities.

One effective strategy is to declare the base button styles with hover, focus, and disabled states. Additionally, you can declare button icon styles once within this base button style. By using CSS custom properties to declare mutable variables within the base button and modifying those properties within each modifier, you can achieve a more consistent way of composing buttons without having to override a modifier's existing override. By aliasing multiple colors to a base color, you can reduce the number of CSS custom properties that need to be modified with each modifier. This approach allows you to mutate multiple properties with a single change, making it a highly effective strategy,

Another handy tip is to use the :is() pseudo-class to simplify your :hover and :focus selectors in the base button declaration. This can make icon selectors more straightforward, resulting in cleaner and more maintainable code. With these tips in mind, you can simplify your button CSS and make it more manageable, maintainable, and effective.

The verbose and increasingly complex way

For the first pass, I worked up these four complete button styles in 100 lines. Not terrible, but could be better. Plus, for each new modifier beyond the existing examples, I’d have to write new :hover , :focus, and svg selectors.

.button {
  background: seagreen;
  display: inline-flex;
  padding: 1rem 2rem;
  color: #fff;
  text-decoration: none;
  transition: all 150ms ease-out;
  border-radius: 5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.875rem;
  font-weight: 600;
  border: 2px solid seagreen;
  align-items: center;
  gap: 0.75rem;
}

.button svg {
  color: darkseagreen;
  transition: color 150ms ease-out;
}

.button:hover,
.button:focus {
  background: forestgreen;
  border-color: forestgreen;
  box-shadow: 5px 5px 0 rgba(34, 139, 34, 0.3);
}

.button:hover svg,
.button:focus svg {
  color: #fff;
}

.button--danger {
  background: tomato;
  border-color: tomato;
}

.button--danger svg {
  color: firebrick;
}

.button--danger:hover,
.button--danger:focus {
  background: firebrick;
  border-color: firebrick;
  box-shadow: 5px 5px 0 rgba(178, 34, 34, 0.3);
}

.button--secondary {
  background: transparent;
  color: seagreen;
}

.button--secondary svg {
  color: seagreen;
}

.button--secondary:hover,
.button--secondary:focus {
  background: rgba(34, 139, 34, 0.1);
  border-color: forestgreen;
  color: forestgreen;
}

.button--secondary:hover svg,
.button--secondary:focus svg {
  color: forestgreen;
}

.button--secondary.button--danger {
  color: tomato;
}

.button--secondary.button--danger svg {
  color: tomato;
}

.button--secondary.button--danger:hover,
.button--secondary.button--danger:focus {
  background: rgba(178, 34, 34, 0.1);
  border-color: firebrick;
  color: firebrick;
}

.button--secondary.button--danger:hover svg,
.button--secondary.button--danger:focus svg {
  color: firebrick;
}

See the Pen Button CSS — The Long Way Around by Patrick Hildebrandt (@phildebrandt) on CodePen.

Better CSS with custom properties

With CSS custom properties, I’m able to declare :hover, :focus, and svg selectors once. Changing the values in each modifier cascades into each selector, reducing our CSS by approximately 35%.

.button {
  --base-color: seagreen;
  --base-color-hover: forestgreen;
  background: var(--bg-color, var(--base-color));
  display: inline-flex;
  padding: 1rem 2rem;
  color: var(--color, #fff);
  text-decoration: none;
  transition: all 150ms ease-out;
  border-radius: 5px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.875rem;
  font-weight: 600;
  border: 2px solid var(--border-color, var(--base-color));
  align-items: center;
  gap: 0.75rem;
}

.button svg {
  color: var(--icon-color, darkseagreen);
  transition: color 150ms ease-out
}

.button:is(:hover, :focus) {
  background: var(--bg-color-hover, var(--base-color-hover));
  border-color: var(--border-color-hover, var(--base-color-hover));
  box-shadow: 5px 5px 0 var(--shadow-color, rgba(34, 139, 34, 0.3));
  color: var(--color-hover, #fff);
}

.button:is(:hover, :focus) svg {
  color: var(--icon-color-hover, currentColor);
}

.button--danger {
  --base-color: tomato;
  --icon-color: lightpink;
  --base-color-hover: firebrick;
  --shadow-color: rgba(220,20,60, 0.3);
}

.button--secondary {
  --bg-color: transparent;
  --color: var(--base-color);
  --icon-color: currentColor;
  --bg-color-hover: rgba(34, 139, 34, 0.05);
  --base-color-hover: forestgreen;
  --color-hover: var(--base-color-hover);
}

.button--secondary.button--danger {
  --bg-color-hover: rgba(255, 250, 240 0.05);
  --base-color-hover: firebrick;
}

See the Pen Button CSS — Working Smarterer by Patrick Hildebrandt (@phildebrandt) on CodePen.