Animations that matters in React

7 min read ☕

🎧   Listen blog while reading   🎧

--:--
--:--

I think hover animations are too underrated. Hover animations are one of the most beautiful ways to make any application feel responsive and dynamic. I agree hover animation is a small thing to consider while creating a big project, but it's these kinds of small details that, in aggregate make a product great.

Any developer can create a simple hover animation with transitions and transform CSS properties, but those animations are not neat and pixel perfect. In this article we will see have to make a simple hover animation look great by using spring functions.

Use case of using spring in CSS animations are infinite. Below are some of the examples I have created.

😁
(Hover over above icons to see the magic.)

Since react is such a robust and flexible framework, which provides a component-driven architecture to implement UI and its elements, we will look into creating flexible components to implement such animations so that they can be used in multiple places.

To start with we will create a component and let's call it <HoverAnimation />.

1const HoverAnimation = ({ rotation = 0, timing = 150, children }) => {
2
3 const [hasAnimated, setHasAnimated] = useState(false)
4
5 const style = {
6 transform: hasAnimated ? `rotate(${rotation}deg)` : `rotate(0deg)`,
7 transition: `transform ${timing}ms`,
8 display: 'inline-block',
9 backfaceVisibility: 'hidden',
10 }
11
12 useEffect(() => {
13 if (!hasAnimated) {
14 return
15 }
16 const timeoutId = window.setTimeout(() => {
17 setHasAnimated(false)
18 }, timing)
19 return () => {
20 window.clearTimeout(timeoutId)
21 }
22 }, [hasAnimated, timing])
23
24 const trigger = () => {
25 setHasAnimated(true)
26 }
27
28 return (
29 <div onMouseEnter={trigger} style={style}>
30 {children}
31 </div>
32 )
33}

Let's break down the code. Basically, we wanted to create a component that will change its state on mouse hover. In doing so we are starting a timer, which eventually sets the component to its original place when the timer end, irrespective of whether the mouse is still hovering over that component or not. This creates a cool-looking animation, not the one which stops animating as soon as you remove your mouse from that component.

To keep the state of animation we have creates a state hasAnimated.

1 const [hasAnimated, setHasAnimated] = useState(false)

We accept a children in component and wrap those children in div, so that we can apply animation and hover event trigger to that element.

1 // used to set hasAnimated value to true and start the animation
2
3 const trigger = () => {
4 setHasAnimated(true)
5 }
6
7 return (
8 <div onMouseEnter={trigger} style={style}>
9 {children}
10 </div>
11 )

On top of this, we have an useEffect hook, which will re-render when hasAnimated changes its value. So practically, when someone hovers over the element, the trigger function will set hasAnimated to true, this will trigger useEffect, which in return start a timer which will set hasAnimated value to false.

1 useEffect(() => {
2 if (!hasAnimated) {
3 return
4 }
5 const timeoutId = window.setTimeout(() => {
6 setHasAnimated(false) // Set hasAnimated value to false after some time
7 }, timing)
8 return () => {
9 window.clearTimeout(timeoutId) // make sure to remove timeout when component is removed from tree.
10 }
11 }, [hasAnimated, timing]) // Only runs when value of hasAnimated changes.

We have talked about hasAnimated but let's see what it actually does. hasAnimated is used inside a style object, which changes the style of component depending on the value of hasAnimated. We rotate the element if hasAnimated is true and move it back to its original place, i.e. 0deg when hasAnimated is false. To animate this nicely we have added transition property to it. Timing for transition is taken as an input for the component to make this component more flexible. Doing so we have full control of how long and up to what degree should the animation go.

1const style = {
2 transform: hasAnimated ? `rotate(${rotation}deg)` : `rotate(0deg)`,
3 transition: `transform ${timing}ms`,
4 display: 'inline-block',
5 backfaceVisibility: 'hidden',
6 }

  • We are using backfaceVisibility: hidden to use hardware acceleration in animations. You can read more about back-face-visibility here.
  • We need to use inline-block instead of inline because inline elements can not be transformed.
  • This is how we will use the HoverAnimation component.

    1 <HoverAnimation rotation={20} timing={200}>
    2 <Icon icon="twitter" />
    3 </HoverAnimation>

    This is what it would look like:

    (Hover over above icons to see the magic.)

    While this is awesome to use with just 3 lines, but it feels robotic and rigid. To make it more natural and fluid, we can add a pinch of spring to it. Let's see how to implement Spring to our component.

    For this, we will be using react-spring. react-spring provides modern hooks to easily add spring effects to your animations.

    To install just do,

    1yarn add react-spring
    2
    3# or if you are an npm person
    4npm install react-spring

    After adding react-spring, we need to modify our HoverAnimation component

    1
    2import { animated, useSpring } from 'react-spring';
    3
    4const HoverAnimation = ({ rotation = 0, timing = 150, children }) => {
    5
    6 const [hasAnimated, setHasAnimated] = React.useState(false);
    7
    8 const style = useSpring({
    9 display: 'inline-block',
    10 backfaceVisibility: 'hidden',
    11 transform: hasAnimated
    12 ? `rotate(${rotation}deg)`
    13 : `rotate(0deg)`,
    14 });
    15
    16 React.useEffect(() => {
    17 // Same as before
    18 }, [hasAnimated, timing]);
    19
    20 const trigger = () => {
    21 // Same as before
    22 };
    23
    24 return (
    25 <animated.span onMouseEnter={trigger} style={style}>
    26 {children}
    27 </animated.span>
    28 );
    29};

    Let's start from the top. We import animated and useSpring hook from react-spring. Now instead of creating an object for style, we will use useSpring and pass the style in this hook. The hook will do its ✨magic✨ and returns the style with spring in it. You can notice that we have not added the transition property in the useSpirng hook, because react-hook handles that for use internally. In a nut-shell react-spring use spring math formulas instead of traditional Bézier curves used by CSS, that's the reason we don't pass transition, to react-string.

    Let's talk about animated from react-spring. The traditional web does not support spring physics natively as of now. So we can not directly use <span> for our spring code, instead, we use <animated.span>, which is similar to HTML's <span> tag, but an additional feature of handling spring object that we have created using useSpring.

    Here is the same example made with react-spring. You be the judge.

    (Hover over above icons to see the magic.)

    To make animations even smoother we can edit spring configuration a bit like this.

    1 const style = useSpring({
    2 display: 'inline-block',
    3 backfaceVisibility: 'hidden',
    4 transform: hasAnimated
    5 ? `rotate(${rotation}deg)`
    6 : `rotate(0deg)`,
    7 config: {
    8 tension: 300,
    9 friction: 10,
    10 },
    11 });

    By lowering the friction and increasing tension we can create something like this too.

    (Hover over above icons to see the magic.)

    Till now we were only animating rotation property, but this component can do a lot better. We can add scale and translate animations without any hassle.

    1
    2const HoverAnimation = ({
    3 x = 0,
    4 y = 0,
    5 scale = 1,
    6 rotation = 0,
    7 timing = 150,
    8 children
    9 }) => {
    10
    11 const style = useSpring({
    12 display: 'inline-block',
    13 backfaceVisibility: 'hidden',
    14 transform: hasAnimated
    15 ? `rotate(${rotation}deg)
    16 translate(${x}px ${y}px)
    17 scale(${scale})`
    18 : `rotate(0deg)
    19 translate(0px 0px)
    20 scale(1)`,
    21 config: {
    22 tension: 350,
    23 friction: 10,
    24 }
    25 });
    26
    27 // ...
    28

    After doing so, now our component will accept scale value, rotation value, and x and y value for translation. We have given default value as their natural value, such that if you don't pass a specific value, then it will not animate.

    This is how we create an element, that animate when we hover over it. But it is the best we can do. Let's see. Can we make something like this with this HoverAnimation component?

    Home
    (Hover over above icons to see the magic.)

    This element will animation even if you don't hover directly onto the home icon. This is where our component will fail. HoverAnimation is built in such a way that it will only animate when that element has hovered. What we are seeing in the above element is animation is triggered when some other element hovers. The best approach to achieve is and make our component more flexible is to convert that component into the custom hook. Let's see what we can achieve by this hook.

    1// hooks/use-animation.js
    2import React from 'react';
    3import { useSpring } from 'react-spring';
    4
    5function useAnimation({
    6 x = 0,
    7 y = 0,
    8 rotation = 0,
    9 scale = 1,
    10 timing = 150,
    11 springConfig = {
    12 tension: 300,
    13 friction: 10,
    14 },
    15}) {
    16 const [isAnimated, setIsAnimated] = React.useState(false);
    17 const style = useSpring({
    18 display: 'inline-block',
    19 backfaceVisibility: 'hidden',
    20 config: springConfig,
    21 transform: isAnimated
    22 ? `translate(${x}px, ${y}px)
    23 scale(${scale})
    24 rotate(${rotation}deg)`
    25 : `translate(0px, 0px)
    26 scale(1)
    27 rotate(0deg)`,
    28 });
    29 React.useEffect(() => {
    30 // same as before
    31 }, [isAnimated]);
    32
    33 const trigger = React.useCallback(() => {
    34 setIsAnimated(true);
    35 }, []);
    36
    37 return [style, trigger];
    38}

    Here we are accepting the object of properties we want to animate along with springConfig and returning a style object which will be eventually used in <animated.div> or <animated.span>, and a trigger function which will toggle isAnimated value. Doing so gives us much more flexibility. For example, now we can call trigger on just on hover but on event handler we want. Let's say, for mobile users you want to animate this on tap, or you want some part of your website to animate after some interval to give emphasis to that part, all this and more can be done with the useAnimation hook. Let's see how to actually use this hook in a component.

    1export const NavItemHoverAnimation = () => {
    2 const [style, trigger] = useAnimation({ y: 5 })
    3
    4 return (
    5 <div onMouseEnter={trigger} >
    6 <span>Home</span>
    7 <animated.span style={style}>
    8 <Icon icon="home" size={20} />
    9 </animated.span>
    10 </div>
    11 )
    12}
    13

    That's all for Hover animations today. You can modify this hook and do all the crazy things you can think of. If you create something cool do let me know on Twitter - Viral Sangani.

    I am planning on publishing one article a week. Consider subscribing to my newsletter for reading more awesome articles.

    GIFY