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:
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;
}
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>
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;
}
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;
}
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);
}
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>
)
}
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)
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 ofdata-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:
Thank you!!