DEV Community

Miki Stanger
Miki Stanger

Posted on

Your Buttons Get Wider on Hover Because You Change Their Font Weight? Here's How to Solve This.

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

You can try it here:

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.

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

// Modifier class
.constant-width-button--larger {
    width: 240px;
}
Enter fullscreen mode Exit fullscreen mode

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

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

<button type="button" data-label="Hover Me Please" class="button">
  <span class="button__label">
    Hover Me Please
  </span>
</button>
Enter fullscreen mode Exit fullscreen mode

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:
: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;
}
Enter fullscreen mode Exit fullscreen mode

The pseudo-element:

  • Takes its text from the data-label.
  • Renders it in bold weight.
  • Is hidden, but its box still affects its surroundings.
.button:before {
  content: attr(data-label);
  font-weight: bold;
  visibility: hidden;
}
Enter fullscreen mode Exit fullscreen mode

The label:

  • positioned absolutely
  • Fits the content area of the button.
.button__label {
  position: absolute;
  top: var(--vertical-padding);
  bottom: var(--vertical-padding);
  left: var(--horizontal-padding);
  right: var(--horizontal-padding);
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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, for coming up with the duplicate label idea and discussing what's the best implementation for it.

Top comments (2)

Collapse
 
madfcat profile image
madfcat • Edited

Nice one! Just registered to leave this comment 😈

UPD: There is a little typo in your code React code: it should be data-label instead of data-text.
I had a various font-family on hover and button width been jumping because the hover font was narrower.
You padding css did not work for me. The text was not centered.
Here is my solution for it:

.button-label {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    text-align: center;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
michaeltwomey profile image
michael-twomey

Thank you!!