Introduction
Hover animations are a great way to make an application feel dynamic and responsive. It's a small thing, but it's exactly the kind of little detail that, in aggregate, can make a product feel great.
Sometimes, though, a simple state change on mouse-enter doesn't quite work. Hover over these icons to see what I mean:
Maybe it's the asymmetry, but these hover states just don't feel good to me ๐ฌ
Instead, what if the icons only popped over to their hover state for a brief moment?
I love this effect. It's playful and dynamic and surprising. It's not commonly done, since it's significantly more complex than using transition
.
It can be used in all kinds of nifty ways. Some examples:
After an informal Twitter poll, it was decided that this effect would be called a "boop".
In this tutorialโwhich is intended for intermediate React usersโwe'll learn how to build it โจ
Link to this headingA first stab
The neat thing about component-driven frameworks like React is that we can package up behaviours in much the same way that we package UI elements. In addition to <Button>
s and <Table>
s, we can create <FadeIn>
s and <SoundEffect>
s.
In our case, the effectโquickly applying and then removing a transformationโcan be divorced from any specific UI elements, so we can apply it to anything!
Here's a first shot at a React component:
jsx
This is a lot of code, so let's walk through it!
The fundamental idea is that when mousing over this element, it flips to an alternative state, just like a typical hover transition. In addition, though, it also starts a timer. When that timer elapses, the state flips back to the "natural" state, regardless of whether we've still hovering or not.
It's a bit like one of those "useless machines" that turns itself off after a short interval:
We keep track of the "boop" state with a state hook, isBooped
.
jsx
We wrap the thing we want to boop โ children
โ in a span. This is so we can apply the rotation style, as well as handle mouse events to trigger the effect in the first place.
jsx
We use an effect hook which is set to fire whenever isBooped
changes. Our hover event causes this value to flip, which causes the effect hook to trigger. The effect hook schedules a timeout to flip isBooped
back to false.
jsx
What about the effect itself? For now, we're limiting it to rotation. When isBooped
is true, we apply a transform: rotate
to the wrapping element.
We control both the rotation amount, in degrees, and the transition length through props, since different situations might call for different effects. We also need to set display
to inline-block
, because inline
elements aren't transformable, and we add backface-visibility: hidden
to take advantage of hardware acceleration.
jsx
Here's how we'd use our new Boop
component:
jsx
And here's what it looks like:
This looks alright, but I know we can do better.
Link to this headingSprings to the rescue!
The motion in this initial version feels robotic and artificial to me. It doesn't have the fluid, organic movement that I crave from modern web animations.
In A Friendly Introduction to Spring Physics, I shared how I add depth and realism to my animations. If you haven't already, I'd suggest checking it out! It features these fun little springy demos:
(โจ Drag and release the weights to see the animation โจ)
My favourite spring-physics animation library is React Spring. It offers a modern hook-based API, and unbeatable performance. Let's update our snippet to use it instead of CSS transitions:
jsx
Before, we were creating a style
object and passing it directly onto our span. Now, we're passing that style object (without transition
) into useSpring
.
The useSpring
hook can be thought of as one of those industrial machines that squirts the strawberry filling into pop-tarts:
In other words, it takes some plain CSS and injects โจ spring magic โจ into it. Instead of using the Bรฉzier curves that CSS provides, it'll use spring math instead. That's why we omit the transition
property; we're delegating that task to React Spring.
Because spring physics aren't a native part of the web (yet!), we can't pass this magic-injected style object onto a <span>
. Instead, we render an <animated.span>
, which is identical to the <span>
we had before, except it knows how to handle the springy style object we've produced.
Here's the result:
This feels a bit sluggish, so let's tweak the configuration:
jsx
By cranking up the tension and lowering the friction, our icons react much more swiftly to being hovered over:
Now we're getting somewhere!
So far, we've limited our boop to affect rotation, but we can do a lot more than that! Let's update it to support size changes (via scale
) and position shifts (via translate
):
The transform
CSS property accepts multiple space-separated values, so our code becomes:
jsx
We default all values to their natural state (eg. 0px translate, 1x scale). This allows us to only specify the values we want to change: if we don't pass a value for rotation, it won't rotate.
I feel pretty happy with this result, but there's a problem with this solutionโฆ And it's a significant one. In fact, we need to rethink our whole approach!
Link to this headingDisconnected boops
On the project I'm working on, I have widgets that can be expanded to show the full content. I thought it'd be fun to cause the caret to skip down a bit on hover:
This presents an interesting challenge, because there's a disconnectโI want the boop to affect only the caret, but it should be triggered whenever I mouse-over any part of it. If I wave my cursor over the word "Show", the caret should boop.

Our current approach doesn't allow for this at all. The animation is bound to the same element as the event-handler.
After some experimentation, I realized that a hook, not a component, was the right API for this effect.
Link to this headingStarting from the consumer
Let's start from the perspective of someone using the API. I'll figure out how to implement it later; first, I want to figure out the simplest, easiest interface.
Here's what I came up with:
jsx
We should be able to pass our hook an object representing the config, and it should give us two things:
- The style object, to be applied to an
animated
element, likeanimated.span
oranimated.button
- A trigger function, to call whenever we want the boop to occur.
If we want, we can apply both of these things to the same element, but we don't have to! In fact, this hook gives us a ton of flexibility: we can trigger it whenever we want, not just on hover. For example, we can include mobile users by setting the effect on tap, or schedule it in an interval to add prominence to an important part of the UI!
Here's how it's implemented:
jsx
Much of this logic is copied over; we're doing the same work to produce that style
object. Instead of applying it onto an element, though, we pass it off to the caller.
Two other small tweaks:
- The spring configuration is now provided as a parameter, since different situations might call for different physics.
- The trigger function is wrapped in
React.useCallback
. This is done so that the function reference doesn't change between renders, to avoid breakinguseMemo
components. Because we don't know how the trigger function will be used, this seems like a prudent bit of forethought.
Link to this headingBack to the component
This hook is neat, but I actually really liked the component API we came up with earlier. In cases where there isn't a disconnect between event-handler and animation, can we use a component instead?
The really cool thing about this pattern is we can easily wrap the hook in a component, to have our cake and eat it too:
jsx
Our Boop component gets a whole lot smaller, since we're delegating all the hard stuff to our useBoop
hook. Now we have access to two glorious APIs, both powered by the same logic. DRY AF.
Link to this headingKeeping it accessible
The component/hook combo we've created is delightful, but delight is subjective. Not everybody wants our UI to dance and jiggle about, especially folks who have a vestibular disorder.
I've written about how to build accessible animations in React. Let's apply some of those lessons here:
jsx
The prefers-reduced-motion hook will let us know if the user has expressed a preference to remove motion. If that value is true
, we'll return a "dummy" style object. This ensures that the element will never move, since the style object is always empty.
Link to this headingYours to discover
First: thank you so much for reading this far!! This has been quite a journey, and I appreciate you for taking it with me ๐
You might be wondering, though: why on earth did we need to cover this in such depth? Why didn't I just publish an NPM module and write a post explaining how to use it, like I did with useSound? Surely that would be more convenient, both for the reader and the author.
Here's the thing: this effect is effective because it's rare. I'm not interested in commoditizing it, because it would lose its charm!
Instead, I'd much rather teach folks how to create effects like this, and let them run with it. This code will live in your Git repo, not buried in a node_modules
folder. Tinker with it, and see what else it can do! Create things I never could have anticipated, and show me on Twitter ๐
This code is mutable, and I hope you'll do some experimentation โจ if you're really feeling adventurous, you could try and incorporate more physics: maybe the element should translate in the same direction as the cursor is moving, as if it was blowing in the breeze?
Here's the final version, ready to copy-and-paste into your repo:
jsx
Link to this headingBonus: That star animation
In the initial demos of this tutorial, I showcased a hoverable star animation:
This effect does indeed use the useBoop
hook we've created, but it also involves some trigonometry, which is beyond the scope of this tutorial. I'm in the process of writing a post about how to use trigonometry to create effects like this oneโif you'd like to receive early access to that tutorial, and others like it, you can sign up for my newsletter:
My newsletter is sent about once a month, and it includes little extras that don't quite fit on this blog. You can, of course, unsubscribe at any time, no hurt feelings. ๐
In the meantime, though, I'll share the snippet, with as much context as I can in the comments! Hope it helps. ๐
jsx
Link to this headingTroubleshooting
If you try to use this effect in your project, and it doesn't work, this section might help you diagnose the issue! If your issue isn't listed, feel free to reach out on Twitter.
Link to this headingNothing happens
If you don't see any motion, and no errors are reported, it's likely that you forgot to use animated
! I still make this mistake frequently.
jsx
Last Updated
June 10th, 2021