I was killing some time browsing my CodePen feed for some eye candy and didn’t need to go past the first page before spotting a neat CSS hover effect by Adam Argyle.
I must’ve spent 10 minutes just staring at the demo in awe. There’s something about this that feels so app-like. I think it might be how contextually accurate it is in that the background color slides in from the left, then exits out through the right. It’s exactly the sort of behavior I’d expect from a mouse-in, mouse-out sort of interaction.
Whatever the case, I fired up a fresh pen and went to work recreating it. And it’s not super complex or anything, but rather a clever use of transitions and transforms paired with proper offsets. Quite elegant! I’m actually a little embarrassed how long it took me to realize how the mouse-out part works.
Here’s how I tackled it, warts and all.
background
.”
“I bet that’s using a transition on a That was my first thought. Define the background-color, set the background-size
and background-position
, then transition the background-position
. That’s how I’ve seen that “growing” background color thing done in the past. I’ve done that myself on some projects, like this:
If I could do the same thing, only from left-to-right, then all that’s left is the mouse-out, right? Nope. The problem is there’s nothing that can really make the background-position
transition from left-to-right to left-to-right. I could make it do one or the other, but not both.
transform
instead.”
“Maybe it’s a My next attempt was jump into transforms. The transform
property provides a bunch of functions that can transition
together for slightly more complex movement. For example, the background
can “grow” or “shrink” by changing the element’s scale()
. Or, in this case, just along the x-axis with scaleX()
.
But like I mentioned, there isn’t a way to isolate the element’s background
to do that. Going from scaleX(0)
to scaleX(1)
scales the entire element, so that basically squishes the link — content and all — down to nothing, then stretches it back out to its natural size which is a totally different effect. Plus, it means starting with scaleX(0)
which hides the whole dang thing by default making it unusable.
But a pseudo-element could work! It doesn’t matter if that gets squished or hidden because it isn’t part of the actual content. Gotta put the background
on that instead and position it directly under the link.
a {
/* Keeps the pseudo-element contained to the element */
position: relative;
}
a::before {
background: #ff9800;
content: "";
inset: 0; /* Logical equivalent to physical offsets */
position: absolute;
transform: scaleX(0); /* Hide by default */
z-index: -1; /* Ensures the link is stacked on top */
}
::before
to change on hover.”
“Now I need I knew I could make ::before
scale from 0
to 1
by chaining it to the link element’s :hover
state.
a:hover::before {
transform: scaleX(1)
}
Nice! I was onto something.
Sprinkle a little transition
fairy dust on it and things start to come to life.
a::before {
background: #ff9800;
content: "";
inset: 0;
position: absolute;
transform: scaleX(0);
transition: transform .5s ease-in-out;
z-index: -1;
}
“Hmm, the transition moves in both directions.”
Again, this is where I sorta got stuck. Something in my head just wasn’t clicking for some reason. As per usual, I ran over to the CSS-Tricks Almanac to see what property might’ve slipped my mind.
Ah, yes. That would be transform-origin
. That allows me to set where the transform
starts, which is not totally dissimilar from setting the background-position
like I tried earlier. The transform
could start from the left instead of its default 50% 50%
position.
a::before {
background: #ff9800;
content: "";
inset: 0;
position: absolute;
transform: scaleX(0);
transform-origin: left;
transition: transform .5s ease-in-out;
z-index: -1;
}
Yeah, like this:
I was already transitioning ::before
to scaleX(1)
on link hover. If I reversed the transform-origin
from left
to right
at the same time, then mayyyybe the highlight goes out the opposite of how it came in when the mouse exits?
a:hover::before {
transform: scaleX(1);
transform-origin: right;
}
🤞
Whoops, backwards! Let’s swap the left
and right
values. 🙃
Gorgeous. Thank you, Adam, for the inspiration!
Here’s a pen of the effect using
background-size
with a linear gradient. The advantage of using background to do this is that it works for links that might span on to multiple lines, like link text within prose. I’ve used this effect on a lot of my projects these past few years.Heck yeah, nice!
I appreciate how this article shows both the thought process and, more importantly, the parts that didn’t work.
A lot of authors like to be the smartest person in the room where the answers just come to them in divine inspiration, so it’s refreshing to see that someone shares my “throw stuff at the wall and fail forward” method I use.
I mean, there are LOTS of issues with this.
The link basically has to be a button. Having it in a block of text breaks it,especially if the link text wraps on to a new line.
If you hover over it quickly repeatedly, the transition jumps to the end before restarting.
… Using borders, or better yet, inset box shadows and expanding their size might help fix the first issue.
Better (tighter) transition statements will probably help the second issue.
Cool dude. Maybe you can write your own post walking through your thought process to make it better. I’d read that.
Seconding that I’d read it if you wrote up an article talking us through how to tighten this up! I’d love to use it and I’d REALLY love for it to be as clean as possible.
Mr Graham,
Haters gonna hate. I really enjoyed the article.
Funnily enough, you can achieve the original effect using just background transitions!
The trick is to use a transition on
background-size
, but notbackground-position
. This way, you can change the background position before the background size animation even begins, and the user will never be able to tell, because the background will always be either completely filled in or completely empty! :DCodepen: https://codepen.io/sloanfinger/pen/XWePVGj
Ha, yeah. My gut reaction felt right but I decided on a different approach when my styles started getting messy. And, of course, I started realizing where I went wrong with the background approach when I was already waist-deep in
transform
. That’s the funny thing about hindsight, but also the great thing about CSS — so many ways to achieve similar things. What’s even funnier is that I landed on the same approach Adam did!Alternatively, you can use a before. And swap the orientation
Nice – I suspect you could do it with inset box-shadows too. The only downside there is you have to set a magic number for how wide the box-shadow goes.
As others have mentioned, using
background
withbackground-position
is definitely the way to go. I wrote a post about 5 different variations that folks might find helpful https://nerdcowboy.com/blog/sliding-underlined-links/I did something similar, but diagonally.
Explained in detail in this article from 2018.
The “animate
transform
, but abruptly fliptransform-origin
” tactic can help with a lot of cool effects in 3D as well, like in this series of demos from early 2016:blocks
flair
[this one had a vice]https://codepen.io/thebabydino/pen/qZJevJ)
a matter of timing
snake motion
Amazing stuff as always, Ana! Thanks for sharing.
Can I challenge you to change the (font) color synchronously?!
Oooo love this. Just wondering if this could change the font colour at the same time to get a fully A11y contrast of colours, while still looking awesome.
–bg-h: what does it mean?