Your Buttons Get Wider on Hover Because You Change Their Font Weight? Here's How to Solve This.
Tags: css, webdev
Published: 2019-07-19 16:27:22

**So,** A while ago, I've had to implement a design that called for buttons that change their `font-weight` on hover. Now here's the problem with this:

![Example of a jumpy button](https://thepracticaldev.s3.amazonaws.com/i/u6r9z0ftiy010hz7dqbg.gif)

You can try it here:
{% codepen https://codepen.io/mimafogeus/pen/RXwaYL %}

This width change is bad. It breaks the layout by pushing adjacent elements, the width change itself feels jumpy, and it all makes for an unpleasant user experience. That's probably not what the designer intended.

Let's solve this.

# Set Constant Width #
Setting the button's width in CSS is the best and easiest solution here... if there are only a few widths you should set.

```css
// Button definition
.constant-width-button {
	width: 120px;
	/* Colors and shape and position and stuff */
}

// Modifier class
.constant-width-button--larger {
	width: 240px;
}
```
However, if you're making a reusable button that has text-dependent, dynamic width, playing around with a margin value that'll fit is a major hassle. It is also prone to breakage with different fonts. Here's how I solved it:



# Duplicate the Label With CSS #
{% codepen https://codepen.io/mimafogeus/pen/jgOybj %}

## The TL;DR ##
- We use CSS to create an invisible pseudo-element that contains a bold version of the text. This element stretches the button, which now has the larger width when you don't hover over it.
- We wrap the element's text with a span, and use absolute positioning to put it over its parent button.
- **🎉🎉🎉PROBLEM SOLVED 🎉🎉🎉**

## You're getting a: ##
- Button component that just works.
- Canonical, valid HTML that shows correctly with or without styling, no matter what reader you use.

**Let's go through the code:**

## HTML ##
```html
<button type="button" data-label="Hover Me Please" class="button">
  <span class="button__label">
    Hover Me Please
  </span>
</button>
```

There are two major additions in the HTML:

* An additional `span` was added around the label. We need that to apply some CSS to it.
* The label was duplicated into a `data-` attribute. This will be used in a `:before` pseudo-element.
You could, theoretically, use that same attribute to generate an `:after` element and just remove the span and its content, but that would not be semantic - if the CSS won't load, the button wouldn't have any label that way.

## CSS ##
### The button itself wasn't changed much: ###
- We added `position: relative` so we could use absolute positioning on the children without them overflowing.
- We moved the padding values to variables, so we could reuse them and be clear about it:
```css
:root {
  --vertical-padding: 5px;
  --horizontal-padding: 10px;
}

.button {
  padding: var(--vertical-padding) var(--horizontal-padding);
  position: relative;
  
  // Design
  background: green;
  border: none;
  color: #fff;
  font-size: 24px;
}

.button:hover {
  font-weight: bold;
}
```

### The pseudo-element: ###
- Takes its text from the data-label.
- Renders it in bold weight.
- Is hidden, but its box still affects its surroundings.
```css
.button:before {
  content: attr(data-label);
  font-weight: bold;
  visibility: hidden;
}
```

### The label: ###
* positioned absolutely
* Fits the content area of the button.
```css
.button__label {
  position: absolute;
  top: var(--vertical-padding);
  bottom: var(--vertical-padding);
  left: var(--horizontal-padding);
  right: var(--horizontal-padding);
}
```

# Example: Implementation in React #
This logic, if you'll use it, will probably be repeated throughout your code. I used it in a component, which allowed me to take care of the label duplication and encapsulate styling.
I used CSS modules in my project, but you can use whatever styling method you prefer.
Also, note that this example assumes that the button's label is a string and not other React elements.
```jsx
import React from 'react'

import styles from './MyButton.module.scss'

export default ({ children, className, ...restOfProps }) => {
  const buttonClassNames = className ? `${styles.button} ${className}` : styles.button

  return (
    <button type='button' className={buttonClassNames} data-text={children} {...restOfProps}>
      <span className={styles.button__label}>{children}</span>
    </button>
  )
}
```

# Conclusion #
Being a stickler for clean HTML and not liking to do CSS tweaks on every reuse of a component, this is the best solution I've found for this problem so far.
Please let me know if you've got better ones in the comments :)

# Thanks #
[Amir Grozki](https://github.com/gerev), for coming up with the duplicate label idea and discussing what's the best implementation for it.