Category Archives: animejs

Converting animated button to React

I was looking at a nice little animation of a button on https://codepen.io/andrewmillen/pen/MoKLob and thought it’d be interesting turning it into a React component especially as I’m currently playing with animejs. As usual such things never “just work” but that also means it’s a good learning process.

So here goes, first off create your React application and add the animejs library…

  • yarn create react-app animatedbutton –typescript

From within the generated application run the following

  • yarn add animejs
  • yarn add @types/animejs

Note: I will not list all the code here, it’s available on GitHub, but I’ll list the changes and things of interest.

Next create a components folder off of the src folder and create AniButton.css, copying the CSS from the original link into that and then create AniButton.jsx which I’ve included here

Note: I used a jsx file as opposed to tsx simply because I didn’t want to spend too long fixing “type” issues initially, but when I did try this I found the ref is not expected on a path element, so we’ll stick with JSX for now.

export class AniButton extends React.Component {

  checkRef = React.createRef();

  constructor(props) {
    super(props);

    this.handleOnClick = this.handleOnClick.bind(this);
  }

  handleOnClick() {

    var pathEls = this.checkRef.current;
    var offset = anime.setDashoffset(pathEls);
    pathEls.setAttribute("stroke-dashoffset", offset);

    var basicTimeline = anime.timeline({
      autoplay: false
    })
    .add({
      targets: ".text",
      duration: 1,
      opacity: 0
    })
    .add({
      targets: ".button",
      duration: 1300,
      height: 10,
      width: 300,
      backgroundColor: "#2B2D2F",
      border: "0",
      borderRadius: 100
    })
    .add({
      targets: ".progress",
      duration: 2000,
      width: 300,
      easing: "linear"
    })
    .add({
      targets: ".button",
      width: 0,
      duration: 1
    })
    .add({
      targets: ".progress",
      width: 80,
      height: 80,
      delay: 500,
      duration: 750,
      borderRadius: 80,
      backgroundColor: "#71DFBE"
    })
    .add({
      targets: pathEls,
      strokeDashoffset: [offset, 0],
      opacity: 1,
      duration: 200,
      easing: "easeInOutSine"
    });

    basicTimeline.play();
  }

  render() {
    return (
      <main>
        <div className="button" onClick={this.handleOnClick}>
          <div className="text" onClick={this.handleOnClick}>Submit</div>
        </div>
        <div className="progress"></div>
        <svg x="0px" y="0px"
          viewBox="0 0 25 30">
          <path ref={this.checkRef} className="check st0" d="M2,19.2C5.9,23.6,9.4,28,9.4,28L23,2" />
        </svg>
      </main>
    )
  }
}

Note: A couple of changes I made to the CSS was change the progress bar name and fixed the background to gray. The reason I did this was simply because I had problems initially with the CSS displaying incorrectly and messed with things to try to resolve – I think the problem is unrelated to the changes I made though.

Also I seem to periodically lose the progress bar, not 100% why but just saying.

The key differences to the original source code are – obviously it’s coded into a React.Component. Event handlers are within the class, but really the main difference is the way we get access to the DOM path element via the ref={this.checkRef}. I’ve removed the loop from the original code which found the paths as we only have one here.

Next, the original code uses the Poppins font, so add the following the public/index.html

<link 
   href="https://fonts.googleapis.com/css?family=Poppins:600" 
   rel="stylesheet">    

The animejs code

At this point everything works nicely, so let’s look at the animejs code.

We need to get a reference to the DOM path object using

var pathEls = this.checkRef.current;
var offset = anime.setDashoffset(pathEls);
pathEls.setAttribute("stroke-dashoffset", offset);

The anime.setDashoffset code does the following

“Creates path drawing animation using the ‘stroke-dashoffset’ property. Set the path ‘dash-offset’ value with anime.setDashoffset() in a from to formatted value.”

Next we create an animejs, which basically allows us to coordinate each stage/step of the animation. Hence the first timeline is to immediately hides the .text using the opacity property and a very short duration.

Now we change the shape of the .button to a thinner button etc.

The next step is to display a progress overlay which starts from the centre (based upon the CSS) and expands out over it’s duration in a linear easing.

Once the previous step’s completed which reduce the .button to zero width to basically hide it and the .progress then changes colour and eventually turns into a circle.

Finally the check/tick is displayed using the strokeDashOffset, so basically this animates the drawing of the check.

CSS

For the most part the animejs timeline code is pretty simple and works a treat, so let’s take a look at the CSS side of things.

Firstly we can import the CSS using

import "./AniButton.css";

Which basically tells webpack (in a standard React application) to include the CSS in the bundle. Now the CSS can be applied to our elements in the usual ways, on an element type or via id or class (in React class becomes classname).

As stated previously though, if you require actual access into the DOM you’ll need to use something like a ref, but use sparingly.

CSS files like the ones we’re using are not really the preferred way to use CSS in React (see the React: CSS in your JS by Christopher Chedeau for some reasons why this is the case, admittedly it’s a few years old but still relevant now).

To save you watching the video, CSS files defines CSS globally which can make styling difficult to debug or understand at times. Also it’s equally difficult to know when CSS is redundant, i.e. an element s no longer used but we still maintain it’s CSS etc.

I’m not going to go into all the others ways to use CSS it this post, but obviously our best way to use CSS would be to have it used locally inlined with our component.

Hence we can instead convert our CSS to the following code within our AniButton.jsx file like this

const styles = {
  main: {
    height: "100vh",
    width: "100vw"
  },
  button: {
    background: "#2B2D2F",
    height: "80px",
    width: "200px",
    textAlign: "center",
    position: "absolute",
    top: "50%",
    transform: "translateY(-50%)",
    left: "0",
    right: "0",
    margin: "0 auto",
    cursor: "pointer",
    borderRadius: "4px"
  },
  text: {
    font: "bold 1.25rem/1 poppins",
    color: "#71DFBE",
    position: "absolute",
    top: "50%",
    transform: "translateY(-52%)",
    left: "0",
    right: "0"
  },
  progress: {
    position: "absolute",
    height: "10px",
    width: "0",
    right: "0",
    top: "50%",
    left: "50%",
    borderRadius: "200px",
    transform: "translateY(-50%) translateX(-50%)",
    background: "gray",
  },
  svg: {
    width: "30px",
    position: "absolute",
    top: "50%",
    transform: "translateY(-50%) translateX(-50%)",
    left: "50%",
    right: "0",
    enableBackground: "new 0 0 25 30"
  },
  check: {
    fill: "none",
    stroke: "#FFFFFF",
    strokeWidth: "3",
    strokeLinecap: "round",
    strokeLinejoin: "round",
    opacity: "0"
  }   
}

Note: underline or hyphens turn into camelCase names and values get wrapped in quotes.

Now we will need to explicitly add the styles to our render code using style={} syntax, so it looks like this

<main style={styles.main}>
  <div className="button" style={styles.button} 
       onClick={this.handleOnClick}>
     <div className="text" style={styles.text} 
       onClick={this.handleOnClick}>Submit</div>
  </div>
  <div className="progress" style={styles.progress}></div>
  <svg x="0px" y="0px"
    viewBox="0 0 25 30" style={styles.svg}>
    <path ref={this.checkRef} style={styles.check} 
      className="check st0" d="M2,19.2C5.9,23.6,9.4,28,9.4,28L23,2" />
  </svg>
</main>

We can now delete our AniButton.css file.

animejs

I was working on some code to display a green circle with a tick (for success) and a red circle with a cross (for failure) within rows in ag-Grid to denote whether service calls succeeded or failed for each row of data. Having the graphics seemed good but what I really wanted was to animate the display of these graphics to make it obvious to the user that these graphics changes/updated.

animejs looked to be a perfect solution to the problem.

There’s lots of great examples using animejs here.

Obviously to begin with I’ve created a React application using the standard scaffolding, and then using yarn installed anime, i.e.

yarn add animejs
yarn add @types/animejs

We’ll create a standard React component (SuccessFailureComponent.tsx) for displaying our new animated component.

import React from 'react';
import anime from "animejs";

interface SuccessFailureProps {
    success: boolean;
}

export class SuccessFailureComponent 
   extends React.Component<SuccessFailureProps, {}> {
      render() {
         // to be implemented
      }
}

In this code we’ve added the property success, which when true will display the green circle with a tick and when false the red circle with the cross.

To begin with we’ll add the following code to draw our graphics to the render function

const backColour = this.props.success ? 
   "#0c3" : 
   "red";
const symbol = this.props.success ? 
   "M9 16l5 5 9-9": 
   "M9 9l14 14 M23 9L9 23";

return (
   <svg className="status" xmlns="http://www.w3.org/2000/svg"
      width="32"
      height="32"
      viewBox="0 0 42 42">
      <circle className="circle"
         cx="16"
         cy="16"
         r="16"
         fill={backColour}/>
      <path d={symbol}
         fill="none"
         stroke="white"
         stroke-width="2.5"   
         stroke-linecap="round"/> :
   </svg>
);

In the above we use the Scalable Vector Graphics element to wrap drawing primitives to first draw a circle with the required fill colour and then using the symbol const which is the drawing of a tick or cross.

If we run the React app at this point (assuming we’ve also added the following component to the App.tsx render method

<SuccessFailureComponent success={false}/>
<SuccessFailureComponent success={true}/>

then we’ll see the two SVG graphics displayed.

Animating our graphics

We’ve come all this way in a post about animation and not animated anything, so let’s now do that. Back in the component, add

componentDidMount() {
   var timeline = anime.timeline({ 
      autoplay: true
   });

   timeline
      .add({
         targets: '.status',
         scale: [{ 
            value: [0, 1], 
            duration: 600, 
            easing: 'easeOutQuad' }
         ]
      });
}

When the application is refreshed you’ll see the graphics scale from nothing to full sized.

In the first line we create the anime timeline which has parameters which will simply start the animation immediately. We then use the add function with the target “status” (our SVG element) to scale the SVG from nothing to full sized with a duration of 600ms. Finally we specify the easing function to use.

We can also add more animation transitions to the timeline, for example the following additions to the timeline code (above) will result in the icons moving right by 10 pixels then returning to where they started

.add({
   targets: '.status',            
   translateX: 10
})
.add({
   targets: '.status',
   translateX: 0
});

Or maybe we’d like to just spin the graphics using

.add({
   targets: '.status',            
   rotate: 360
});

There’s more than just the timeline function, there’s also stagger which allows the animation of multiple elements. Also keyframes which allows us to create an array of keyframes for our animations.

This easings.net is a great resource for seeing how different easing functions might look. If you want to (for example) add a little bounce back to the easing try, easeOutBack.

The anime documentation also has some great examples along with the documentation of fairly simple transitions.